Swift / SwiftUI — Distribution¶
OWNER: martijn, valentin ALSO_USED_BY: alexander (design integration) LAST_VERIFIED: 2026-03-26 GE_STACK_VERSION: Xcode 26 / Fastlane / GitHub Actions
Overview¶
GE uses Fastlane with GitHub Actions for automated iOS builds, signing, and distribution. This page covers code signing, TestFlight, App Store submission, and CI/CD pipeline setup. Every GE iOS project follows this pipeline — no manual Xcode uploads.
Code Signing¶
Overview¶
Apple requires all apps to be cryptographically signed before they can run on devices. Two components: signing certificate (identity) and provisioning profile (entitlements + devices).
Fastlane Match (GE Standard)¶
GE uses fastlane match to share signing identities across the team.
Certificates and profiles are stored in a private Git repository.
# Matchfile
git_url("git@github.com:growing-europe/ios-certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.growingeurope.clientapp"])
username("ios@growingeurope.com")
CHECK: Agent is setting up code signing for a new project.
IF: Agent generates certificates manually in Xcode or Apple Developer Portal.
THEN: Stop. Use fastlane match to generate and store certificates in the shared repo.
Match Commands¶
# Generate development certificates + profiles
fastlane match development
# Generate App Store distribution certificates + profiles
fastlane match appstore
# Generate Ad Hoc profiles (for TestFlight alternatives)
fastlane match adhoc
# Nuclear option — revoke all and regenerate
# WARNING: disables all existing Ad Hoc/Enterprise builds
fastlane match nuke distribution
fastlane match appstore
ANTI_PATTERN: Running match nuke without team notification.
FIX: match nuke revokes ALL certificates. Notify the team first — existing Ad Hoc builds stop working.
Automatic vs Manual Signing¶
GE convention:
- Debug/Development — Automatic signing (Xcode managed).
- Release/Distribution — Manual signing via Fastlane Match profiles.
# Fastfile — set signing for release builds
lane :release do
sync_code_signing(type: "appstore", readonly: true)
build_app(
scheme: "ClientApp",
export_method: "app-store",
export_options: {
signingStyle: "manual",
provisioningProfiles: {
"com.growingeurope.clientapp" => "match AppStore com.growingeurope.clientapp"
}
}
)
end
Provisioning Profiles¶
Types¶
| Profile | Use Case | Devices |
|---|---|---|
| Development | Debug builds on registered devices | Up to 100 |
| Ad Hoc | Testing builds distributed outside TestFlight | Up to 100 |
| App Store | Production distribution | All (via App Store) |
| Enterprise | In-house distribution (requires Enterprise account) | Unlimited |
CHECK: Agent is configuring a provisioning profile.
IF: Project uses wildcard bundle ID (com.growingeurope.*).
THEN: Use explicit bundle ID (com.growingeurope.clientapp). Wildcards cannot use push notifications, App Groups, or Sign in with Apple.
TestFlight¶
Automated Upload¶
lane :beta do
sync_code_signing(type: "appstore", readonly: true)
increment_build_number(
build_number: ENV["GITHUB_RUN_NUMBER"]
)
build_app(scheme: "ClientApp", export_method: "app-store")
upload_to_testflight(
skip_waiting_for_build_processing: true,
distribute_external: false
)
end
TestFlight Groups¶
- Internal — GE team, auto-distributed on upload, no review required.
- External (Client) — Requires Beta App Review on first build, then auto-distributed.
CHECK: Agent is distributing a TestFlight build. IF: Build goes to external testers for the first time. THEN: Expect 24-48h delay for Beta App Review. Include beta description and contact info.
App Store Submission¶
Metadata Management¶
lane :release do
sync_code_signing(type: "appstore", readonly: true)
build_app(scheme: "ClientApp", export_method: "app-store")
upload_to_app_store(
submit_for_review: true,
automatic_release: false, # Client approves release timing
force: true, # Skip HTML report
precheck_include_in_app_purchases: false
)
end
Required Metadata¶
| Field | Notes |
|---|---|
| App name | Max 30 characters |
| Subtitle | Max 30 characters |
| Description | Up to 4000 characters |
| Keywords | Max 100 characters total, comma-separated |
| Screenshots | Min 1 per required device size (6.5" or 6.9" iPhone, 13" iPad) |
| Privacy policy URL | MANDATORY — must be publicly accessible |
| Support URL | MANDATORY |
| Category | Primary + optional secondary |
| Age rating | Questionnaire in App Store Connect |
| App Review notes | Login credentials for review team if auth is required |
App Store Connect API Key¶
GE uses API keys (not Apple ID + password) for automated uploads.
# Appfile
app_identifier("com.growingeurope.clientapp")
itc_team_id("YOUR_TEAM_ID")
# API key stored in CI secrets
lane :release do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_filepath: ENV["ASC_KEY_PATH"]
)
upload_to_app_store(api_key: api_key)
end
CI/CD with GitHub Actions¶
Workflow Structure¶
name: iOS Build & Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.app
- name: Install dependencies
run: |
brew install fastlane
swift package resolve
- name: Run tests
run: fastlane test
deploy_testflight:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.app
- name: Install Fastlane
run: brew install fastlane
- name: Deploy to TestFlight
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_AUTH }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_PATH: ${{ secrets.ASC_KEY_PATH }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
fastlane match appstore --readonly
fastlane beta
CI-Specific Fastlane Setup¶
# Fastfile
before_all do
if is_ci
setup_ci # Creates temporary keychain for CI
end
end
lane :test do
run_tests(
scheme: "ClientApp",
devices: ["iPhone 16"],
clean: true
)
end
CHECK: Agent is configuring CI.
IF: CI uses match without readonly: true.
THEN: Add readonly: true (or is_ci guard). CI should NEVER generate new certificates — only use existing ones.
Secrets Management¶
| Secret | Purpose |
|---|---|
MATCH_PASSWORD |
Decrypts certificates from Git repo |
MATCH_GIT_BASIC_AUTHORIZATION |
Base64-encoded user:token for HTTPS Git clone |
ASC_KEY_ID |
App Store Connect API key ID |
ASC_ISSUER_ID |
App Store Connect issuer ID |
ASC_KEY_PATH |
Path to .p8 key file (stored as secret file) |
ANTI_PATTERN: Committing .p8 key files, certificates, or passwords to the app repository.
FIX: Store in GitHub Actions secrets. The .p8 file is written to a temp path during CI only.
Version Numbering¶
GE convention:
- Version (
CFBundleShortVersionString): Semantic versioning —1.2.3. - Build (
CFBundleVersion): GitHub Actions run number — auto-incremented.
GE-Specific Conventions¶
- Fastlane Match for all signing — No manual certificate management.
- GitHub Actions for CI/CD — No Jenkins, no CircleCI, no Bitrise.
- TestFlight for all beta distribution — No Ad Hoc unless client-required.
- API keys over Apple ID auth — More secure, no 2FA prompts in CI.
- Client approves release timing —
automatic_release: falseby default.
Cross-References¶
READ_ALSO: wiki/docs/stack/swift-swiftui/index.md READ_ALSO: wiki/docs/stack/swift-swiftui/checklist.md READ_ALSO: wiki/docs/stack/swift-swiftui/pitfalls.md