Building Your App
이 콘텐츠는 아직 번역되지 않았습니다.
This section covers the process from an empty repository through a working App Fair project that builds and runs on both iOS and Android. It assumes that the proposed application has already been evaluated against the Inclusion Criteria.
Project model
Section titled “Project model”Every App Fair app is a conventional Skip project in Skip Lite mode:
- A single Swift codebase.
- Native iOS app built from SwiftUI source.
- Native Android app transpiled to Kotlin and Jetpack Compose. There is no embedded browser, no shared runtime, and no compromise on platform behaviour.
- Organized as a Swift Package, an Xcode project for iOS, and a Gradle project for Android, kept in sync by Skip’s tooling.
Skip has its own documentation, which this guide does not duplicate. The reference for the full project layout is Conventional Skip Projects, and the on-ramp for first-time Skip users is Getting Started. The sections below focus on the App Fair-specific configuration on top of that foundation.
Step 1: Create a GitHub organization
Section titled “Step 1: Create a GitHub organization”Every App Fair app must live in its own GitHub organization, including projects with a single maintainer. The organization-per-app convention makes the app a self-contained unit that can be transferred, co-maintained, or rescued by the community without entangling personal accounts.
A free organization can be created at github.com/account/organizations/new?plan=free. The organization name is the app token: the immutable identifier the App Fair uses for the app’s source. It is typically written in Title-Case-With-Dashes (e.g. Faire-Games).
The app token is distinct from the app’s displayed title, which is the localizable string the user sees on their device’s home screen and in store listings. For example, the Faire-Games repository ships under the displayed title “Fair Games”; the token (Faire-Games) is the identifier that appears in the GitHub URL and that never changes, while the title is set in Skip.env and the Fastlane metadata and may change over time (for example to resolve a trademark dispute, to update branding, or to translate into another language).
Step 2: Create the repository
Section titled “Step 2: Create the repository”A public repository should be created inside the new organization at github.com/new. Two constraints apply:
- The repository name must match the app token (and therefore the organization name) exactly. For the
Faire-Gamestoken, the repository must also beFaire-Games, yielding the canonical URLgithub.com/Faire-Games/Faire-Games. This convention makes the fork-based distribution model unambiguous. - The repository must be empty (no README, no
.gitignore, no auto-generatedLICENSE). Theskip init --appfaircommand in the next step populates the repository, including theLICENSE.GPLfile required by the App Fair.
Step 3: Initialize the Skip project
Section titled “Step 3: Initialize the Skip project”First-time Skip users should complete the Skip Getting Started workflow to install Xcode, the Skip CLI, and the Android tooling. Once skip checkup reports a clean environment, the project can be initialized:
skip init --appfair --transpiled-app --appid=org.appfair.app.Faire-Games Faire-Games FaireGamesUI FaireGamesModelThe positional arguments name the project: first the app token (Faire-Games), then the UI Swift Package target (FaireGamesUI), then the model target (FaireGamesModel). The flags configure the project for App Fair distribution:
--appfair: enables App Fair-conventional defaults — namespaced bundle ID,LICENSE.GPL,appfair/-fork release pipeline, and theappfair/appfair-apppackage dependency.--transpiled-app: explicitly selects Skip Lite, the only mode App Fair apps may use.--appid=…: sets the upstream bundle identifier written intoSkip.env. For App Fair distribution theappfair/fork rewrites this toorg.appfair.app.<token>at sign time, so the value here is most useful as the development team’s own ID for sideloading onto a real iOS device. See Bundle identifiers below.
The initializer:
- Configures
Package.swift,Skip.env, and the Xcode and Gradle projects with App Fair-conventional defaults (App Fair-namespaced bundle ID, marketing version, etc.). - Adds a
LICENSE.GPLfile (GNU GPL v2.0-or-later). - Adds the
appfair/appfair-appSwift package dependency for sharedAppFairUIcomponents. - Generates a
.github/workflows/<app>.ymlworkflow that invokes theskiptools/actionsreusable build pipeline. - Scaffolds the
fastlane/metadata directories for iOS and Android.
The resulting project should be pushed to the repository:
cd Faire-Gamesgit initgit remote add origin https://github.com/Faire-Games/Faire-Games.gitgit branch -M maingit add .git commit -m "initial commit"git push -u origin mainThe repository now contains a valid (though minimal) App Fair app. Each push to main triggers the CI workflow, which builds the iOS and Android targets.
Project layout
Section titled “Project layout”The conventional Skip project layout is documented in detail on the Skip docs site. The Faire-Games repository is a worked example. A brief orientation:
Faire-Games/├── Package.swift ← Swift Package Manager manifest├── Package.resolved├── Skip.env ← Cross-platform config (bundle ID, version)├── LICENSE.GPL ← GPL-2.0-or-later, required├── README.md├── Sources/ ← Swift code (shared by iOS and Android)├── Tests/ ← XCTest tests, also run on Android via Robolectric├── Darwin/ ← iOS-specific files│ ├── FaireGames.xcconfig│ ├── Entitlements.plist│ ├── Info.plist│ ├── Assets.xcassets/│ └── fastlane/ ← App Store metadata and screenshots└── Android/ ← Android-specific files ├── settings.gradle.kts ├── app/ └── fastlane/ ← Play Store metadata and screenshotsSeveral points are worth noting:
Skip.envis the source of truth for cross-platform settings. Bundle identifier, marketing version (e.g.1.4.0), build number, Android package name, and the App Store / Play Store IDs are all defined here. TheMARKETING_VERSIONandCURRENT_PROJECT_VERSIONvalues must be updated here prior to tagging a release.- The Swift sources are shared between platforms. Skip handles transpilation to Kotlin and Jetpack Compose. Where platform-specific code is genuinely required, the
#if SKIPand#if !SKIPdirectives can be used to branch. - Fastlane metadata is checked in under
Darwin/fastlane/andAndroid/fastlane/. The contents of these directories are described in Submitting Your App. The general-purpose references are the Fastlane documentation and the Skip Fastlane deployment guide.
Bundle identifiers
Section titled “Bundle identifiers”Every App Fair app published through the appfair/ fork is built with a canonical bundle identifier of org.appfair.app.<token>. For the Faire-Games token this means the production binary ships with:
- iOS
PRODUCT_BUNDLE_IDENTIFIER=org.appfair.app.Faire-Games - Android package name =
org.appfair.app.Faire_Games(underscores replace dashes, since Java package segments cannot contain hyphens)
The Skip.env value in the source repository is the upstream bundle identifier, used during local development. It can be set to anything the developer team controls — for example com.example.fairegames under the developer’s own Apple Developer team — which is what makes it possible to sideload and run the app on a real iOS device using the developer’s own signing identity. The appfair/ fork rewrites this value to the canonical org.appfair.app.<token> form before signing the production binary, so the upstream bundle ID never reaches end users through the App Fair.
Development workflow
Section titled “Development workflow”Day-to-day development follows a standard SwiftUI workflow:
- Open the project in Xcode (via
Project.xcworkspaceat the repository root, or withskip app launchfrom the terminal). Swift and SwiftUI development proceeds as normal; the Android target is kept in sync automatically. - Run the app on the iOS Simulator directly from Xcode.
- Run the app on the Android emulator by first launching an emulator from Android Studio’s Device Manager, then building from Xcode. The “Launch Android APK” build phase deploys the transpiled Kotlin app to the running emulator. iOS logs appear in Xcode’s console; Android logs appear in Android Studio’s Logcat tab.
- Commit and push. Each push triggers the CI workflow, which builds both targets and surfaces cross-platform regressions early.
- Iterate. Pull Requests are recommended for non-trivial changes so that the CI build runs against the PR prior to merge.
The full development reference, including parity testing, FFI, and Kotlin interop, is in the Skip documentation. The Skip Lite vs. Skip Fuse distinction is described there for completeness; App Fair apps use Skip Lite, so the Skip Fuse documentation is not applicable.
Designing for the Four Cornerstones
Section titled “Designing for the Four Cornerstones”The four cornerstones (transparent, ubiquitous, global, and accessible) are practical engineering constraints. Each is significantly easier to satisfy when designed for from the first commit than when retrofitted later. The remainder of this section translates each cornerstone into specific implementation guidance. (Transparent is handled by the project’s licence and dependency choices, covered separately under Licensing, so the rest of this section focuses on Ubiquitous, Global, and Accessible.)
Localization (Global)
Section titled “Localization (Global)”App Fair apps are designed for a global audience, so every user-facing string must be localizable. The recommended mechanism is a Localizable.xcstrings String Catalog: Xcode’s structured, JSON-backed format for localized strings, which supersedes the older .strings/.stringsdict pair.
In practice:
- Use
Text("Some string")andLocalizedStringKey(or, for resources in a SPM target,Text("Some string", bundle: .module)) rather thanText(verbatim: "…"). Strings written this way are picked up automatically by Xcode’s string catalog generator. - Keep one
Localizable.xcstringsper target, located underSources/<Module>/Resources/. The catalog is edited in Xcode and supports per-locale plural variants and per-string review states. - Use string-format substitutions (
"Hello, %@") rather than string concatenation, so translators can reorder arguments to match the grammar of the target language. - Design every screen as if it had to accommodate Spanish, German, or Russian (each of which expands significantly relative to English), as well as Arabic or Hebrew (which read right-to-left).
The Localizable.xcstrings catalog is bridged to Android’s strings.xml resource system automatically. A translation contributed once applies correctly on iOS and Android, including bidirectional layout flipping for RTL languages and per-locale formatting of dates, numbers, and currencies.
Minimum supported languages
Section titled “Minimum supported languages”Every App Fair app must ship with at least English and French translations. This is not aimed at coverage (any catalogue serving a global audience needs many more locales than two), but at verification: maintaining a second locale from day one forces the maintainer to confirm that every user-facing string is actually externalized and that the app launches cleanly under a non-default locale. A project that runs only under en-US is one where a hard-coded English string can hide for months before a non-English user notices.
The recommended workflow is to add English as the development locale, add French as the second locale in the Localizable.xcstrings catalogue, and run the app at least once with the iOS Simulator and Android emulator set to French before each release. Any string that appears in English while the device is set to French is a missing translation, a hard-coded literal, or a localization bug — all of which the dual-locale launch surfaces immediately. Additional locales beyond English and French are strongly encouraged and can be added by community contributors over time.
Accessibility (Accessible)
Section titled “Accessibility (Accessible)”App Fair apps should function for users across the spectrum of abilities, including users of VoiceOver and TalkBack, users of Dynamic Type, users of Switch Control, and users who depend on sufficient colour contrast. SwiftUI provides most of the necessary infrastructure through its accessibility view modifiers, which are translated into the equivalent Jetpack Compose accessibility semantics on Android. A UI element described once is accessible on both platforms.
The most frequently used modifiers:
.accessibilityLabel("…"): the text spoken by a screen reader. Essential for icon-only buttons, custom drawing, and any element that is not text..accessibilityHint("…"): additional context (e.g."Double-tap to play")..accessibilityValue("…"): the current value of a control (e.g. the position of a slider)..accessibilityAddTraits(.isButton)/.isHeader/.isSelected: semantic role hints..accessibilityElement(children: .combine): groups child views into one accessible element when they make sense together..accessibilityHidden(true): excludes decorative elements (such as background imagery) from screen-reader traversal.
Beyond the modifiers, several design practices matter:
- Support Dynamic Type. Use semantic text styles (
.body,.headline,.caption) rather than fixed point sizes, and test at the largest accessibility text sizes (which is where layouts typically break first). - Avoid relying on colour alone. Pair colour cues with shape, icon, or text so that colour-blind users receive the same information.
- Use tap targets of at least 44×44 points.
- Test with VoiceOver and TalkBack enabled. Walk through the primary user flows with the screen reader active. A UI that cannot be navigated by ear cannot be navigated by a blind user.
- Apply
accessibilityIdentifierto key views to support UI tests and cross-platform parity testing.
Additional development practices
Section titled “Additional development practices”Beyond the four cornerstones, the following practices are worth observing.
Network conditions
Section titled “Network conditions”App Fair apps reach users on flaky cellular connections, old Wi-Fi, and gigabit fibre alike. The app should be tested with the network conditioner set to “Edge” and “3G” at least once. An offline mode that degrades gracefully is a strong default.
Power consumption
Section titled “Power consumption”Avoid background work the user has not requested. Avoid polling. Avoid burning CPU on animations that no user is currently observing. A significant fraction of App Fair users run older devices in regions where battery life and cellular data are constrained.
The AppFairUI components
Section titled “The AppFairUI components”Every App Fair app depends on the appfair-app package, which exposes the AppFairUI module. The dependency is added automatically by skip init --appfair and is mandatory for every catalog app.
The package supplies the shared About screen, attribution metadata, third-party license rows, the “Made with Skip / Distributed through the App Fair” credits, and the AppFairSettings wrapper described below.
Settings screen
Section titled “Settings screen”Every App Fair app must expose a prominent settings screen, reachable from one of:
- a top-level tab in the app’s primary
TabView, or - a top-level toolbar button on the primary screen, presented either inline or as a modal sheet.
The settings view must wrap its contents in an AppFairSettings(bundle: .module) { ... } block:
import SwiftUIimport AppFairUI
struct SettingsView: View { @AppStorage("appearance") var appearance: String = ""
var body: some View { AppFairSettings(bundle: .module) { Section("Appearance") { Picker("Theme", selection: $appearance) { Text("System").tag("") Text("Light").tag("light") Text("Dark").tag("dark") } } } }}The wrapper inserts the standard App Fair rows (about, contributors, third-party licenses, App Fair Project links, and locale preferences) around the app’s own configuration sections, producing a consistent settings experience across the catalog. Use of the wrapper is required, not opt-in.
Once the application implements its core functionality, proceed to Submitting Your App.