ImageRenderer draws the real views with demo data, since the watch simulator is not drivable here. README covers the widget target and how to add it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 |
WristCodeWidget |
org.gbrown.wristcode.watchkitapp.widget |
The complication, embedded in the watch app |
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. The complication and the multi-select question
are rendered by SnapshotTests instead, which only run when given a directory:
TEST_RUNNER_WC_SCREENSHOT_DIR=$PWD/screenshots xcodebuild -scheme WristCode \
-destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' test \
-only-testing:WristCodeTests/SnapshotTests
In demo mode the complication shows the demo counts too.
Complication
WristCodeWidget shows how many threads wait on you, from GET /v1/summary, in the circular,
rectangular, inline and corner families. Tapping it opens the app, at the longest-waiting thread when
there is one (wristcode://thread/<hostId>/<threadId>). WidgetKit refreshes it about every 15 minutes;
the app reloads it sooner when its feed counts change, when a push arrives in the foreground, and
after a notification action.
The app and the extension share the App Group group.org.gbrown.wristcode. The device token and
gateway URL live in that group's Keychain access group, and the demo flag and last summary in its
defaults. Builds before the complication kept the token in the app's own Keychain group;
DeviceStore.migrateToSharedGroup() moves it on launch, so a paired watch stays paired.
To add it: long-press the watch face, Edit, swipe to Complications, pick a slot, then WristCode.
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 |
API/Route.swift |
Screens the app can open, and their wristcode:// URLs |
Auth/DeviceStore.swift |
Device token and gateway URL in the shared Keychain group |
Shared/SharedState.swift |
Demo flag and last summary in the App Group, for the complication |
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 |
../WristCodeWidget/ |
The complication: timeline provider, and one view per family |