The App Store only accepts a watch-only app inside an empty iOS stub (watchapp2-container), the pair Xcode's Watch-only App template makes. The stub takes org.gbrown.wristcode; the watch app, and so the APNs topic, becomes org.gbrown.wristcode.watchkitapp. Adds ITSAppUsesNonExemptEncryption = NO and scripts/testflight.sh, which archives with API key automatic signing and a timestamp build number, then exports and uploads (or exports only with --no-upload). Co-Authored-By: Claude Opus 5.5 <[email protected]>
3.0 KiB
WristCode watch app
SwiftUI, watch only, Swift 6 with strict concurrency, and no dependencies. It talks to the gateway
over HTTPS + JSON as described in docs/api.md; WristCode/API/Models.swift
mirrors gateway/src/contract.ts.
Build and test
cd watch
xcodegen generate # the .xcodeproj is generated and gitignored
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' build
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' test
TestFlight
The App Store only takes a watch-only app inside an empty iOS stub, so project.yml has two app
targets, the same pair Xcode's "Watch-only App" template makes:
| Target | Bundle ID | Job |
|---|---|---|
WristCodeContainer |
org.gbrown.wristcode |
iOS stub that owns the App Store Connect record and embeds the watch app |
WristCode |
org.gbrown.wristcode.watchkitapp |
The watch app, and the APNs topic |
scripts/testflight.sh # archive, export and upload to App Store Connect
scripts/testflight.sh --no-upload # archive and export an .ipa to build/export-<n> only
The build number defaults to a YYYYMMDDHHMM timestamp (override it with BUILD_NUMBER). Signing is
automatic through the App Store Connect API key in ASC_KEY_PATH, ASC_KEY_ID and ASC_ISSUER_ID,
which default to the key in ~/.appstoreconnect/private_keys. The entitlements file says
aps-environment: development; the App Store export re-signs it as production. Release builds
register their token with the gateway as production, Debug builds as sandbox.
Demo mode
DemoGateway serves fixture data shaped like the docs/api.md examples, and it reacts to writes:
Allow moves a thread to working, a reply restarts a finished thread. The SwiftUI previews use it,
the pairing screen has a "Try the demo" button, and launch arguments open a screen directly:
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo # feed
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -thread littleman/t-next # thread
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen new # new thread
screenshots/ holds the screens captured this way.
Layout
| Path | Job |
|---|---|
WristCodeApp.swift |
App entry, root navigation, WKApplicationDelegate for APNs registration |
AppModel.swift |
Paired, demo or unpaired; navigation path; the feed; 401 sends you back to pairing |
API/GatewayClient.swift |
GatewayAPI protocol and the URLSession client |
API/Models.swift |
Codable mirror of the contract |
Auth/DeviceStore.swift |
Device token and gateway URL in the Keychain |
Demo/DemoGateway.swift |
In-memory gateway with fixtures |
Views/ |
Pair, Feed, Thread, New thread, and the mock B theme |
Notifications/NotificationHandler.swift |
Categories approval, finished, failed, input, host and their actions |