No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
a.ryzhak 1ba355230f Nothing but the flavour chooses the platform
PlatformAPI had a `baseURL` fixed to development, used by nothing. A
constant with that name sitting among the endpoint definitions is a trap:
the next call site to reach for it would have talked to dev.numuhq.tech
from the App Store build, and nothing would have said so.

Gone, with a note where it was. Every endpoint is a path with no host in
it, and the single `#if DEV_PLATFORM` in AppContainer is the only thing
that picks a host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-19 19:20:44 +03:00
NUMUFieldApp Two apps, two build kinds, six configurations 2026-10-19 18:39:37 +03:00
NUMUFieldAppUITests The version was on screen the whole time, from a source nobody read 2026-10-14 22:00:41 +03:00
Packages Nothing but the flavour chooses the platform 2026-10-19 19:20:44 +03:00
.gitignore Ignore the generated docs folder, not every document 2026-10-06 04:05:17 +03:00
project.yml Two apps, two build kinds, six configurations 2026-10-19 18:39:37 +03:00
README.md Two apps, two build kinds, six configurations 2026-10-19 18:39:37 +03:00

Okhama — NUMU CX Field App

Shift control and badge data transfer for frontline retail workers. Native iOS/iPadOS, SwiftUI, iPad-first, built on a small unidirectional-data-flow runtime and split into Swift packages.

Setup

The Xcode project is generated, not checked in. project.yml is the source of truth; NUMUFieldApp.xcodeproj is built from it by XcodeGen and is in .gitignore. A fresh clone has no project file until you generate one.

brew install xcodegen
xcodegen generate && open NUMUFieldApp.xcodeproj

Run xcodegen generate again after pulling changes that touch project.yml, and after adding or deleting source files — they are picked up from the directory tree, so no project file needs editing by hand.

Changes made through Xcode's UI are discarded by the next generate. Signing team, capabilities, build settings, Info.plist keys and the version number live in project.yml. Editing the generated project instead is the one mistake this layout invites: a version set in Xcode's General tab reverts silently, and the archive goes out with the old one.

The signing team is the usual way to meet that mistake, so it is set once at the top of project.yml under settings.base rather than on a target — every target has to carry it, the UI test bundle included, because that bundle is signed too and both schemes build it. A team set on the app target alone leaves the test target unsigned, and Xcode asks for one on every build. After editing it, run xcodegen generate: until you do, the project still holds the old value and the prompt keeps coming back.

Requirements: a Mac with Homebrew and Xcode 16 or later (the suites use Swift Testing); developed against Xcode 27.0. Deployment target is iOS 17.0. No other dependencies — every package is local.

Apps, schemes and configurations

Two apps are built from this one target, and each is built two ways. That is two dimensions, so six configurations and four schemes — and every name states both, because the one thing worse than six configurations is three that mean different things depending on what you remember.

App Bundle id Platform App Store Connect
Okhama com.numuhq.fieldapp numuhq.tech the live record
NUMU Frontliner com.numuhq.frontliner dev.numuhq.tech its own record

They are separate apps on purpose: separate bundle ids mean separate records, separate TestFlight, and — the point — both installed on one device at once, with their own keychain items, their own UserDefaults and their own Documents. They share an icon, so the home-screen label is what tells them apart.

Build kind What it is
Debug Unoptimised, for Xcode.
Beta Optimised, plus INTERNAL_TOOLS: the debug switcher, the mock repositories and the stub badge. TestFlight.
Release Optimised, and nothing else.

The configurations are every pairing — Debug Dev, Debug Prod, Beta Dev, Beta Prod, Release Dev, Release Prod — and the four schemes are the four things that get uploaded:

Scheme Runs Archives
Okhama Debug Prod Release Prod — App Store submissions.
Okhama Beta Beta Prod Beta Prod — TestFlight for the live app.
NUMU Frontliner Debug Dev Release Dev
NUMU Frontliner Beta Beta Dev Beta Dev — TestFlight for the dev app, and the scheme for developing against the mocks.

Develop with NUMU Frontliner. The Okhama scheme runs Debug Prod, which talks to the live platform: running it from Xcode writes to a worker's real data. That it is possible at all is deliberate — debugging against live data is sometimes the only way to see a problem — but it should be a choice, not what you get by opening the project.

The store schemes are kept apart from the Beta ones for the same reason. If Okhama archived a Beta configuration, a submission made the usual way would quietly ship the mocks and the maintenance tooling with it.

Which platform a build talks to is decided once, in AppContainer, from DEV_PLATFORM. It cannot live beside the URLs in PlatformEnvironment, because the Swift packages never see the app target's compilation conditions: a #if there would give the same answer in every build. INTERNAL_TOOLS is set the same way and read by nothing else. The two used to be one — the platform was implied by the build kind — which is why "an optimised beta against the live platform" could not be asked for.

Adding a flavour

Everything that differs between the two apps is in project.yml, under the app target's settings.configs: the bundle id, APP_DISPLAY_NAME (which the generated Info.plist reads for CFBundleDisplayName), and the compilation conditions. Nothing in the source tree names a flavour; the two places that knew an app's identity now read it — KeychainStore's service is Bundle.main.bundleIdentifier, and the debug panel's "Files → …" text is the display name.

SWIFT_ACTIVE_COMPILATION_CONDITIONS is written whole in each configuration, DEBUG included. It is otherwise supplied by the configuration's type, and that default is lost the moment the key is set at all.

Version

Both numbers live in project.yml, under targets.NUMUFieldApp.settings.base:

MARKETING_VERSION: "1.1.12"
CURRENT_PROJECT_VERSION: "60"

Raise CURRENT_PROJECT_VERSION for every upload — App Store Connect rejects a build number it has already seen — then xcodegen generate before archiving. The block above is the real current version, not an example: update it in the same commit as the bump.

Packages

Package Contents
Core Architecture (the store/reducer/effect runtime), Networking, Persistence
Domain Models, policies and the protocols the features talk to
Data Repositories, the platform API client, the badge adapter, mocks
BadgeLink BadgeProtocol (wire format) and BadgeLink (CoreBluetooth and a stub)
DesignSystem Colours, type, components
Features FeatureKit plus the feature modules, assembled by AppFeature

Nothing in BadgeLink knows about this app — it speaks badge vocabulary so it can be lifted into other tooling. Data/BadgeLink/BadgeLinkService is the one place that translates between the two.

Tests

Each package tests on its own. The umbrella scheme is <Package>-Package where one exists (Core, BadgeLink, Features); the others are named after the package.

cd Packages/Features && xcodebuild test -scheme Features-Package -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M5)'
for p in Domain Data; do (cd Packages/$p && xcodebuild test -scheme $p -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M5)'); done

DesignSystem has no suite of its own — it is looked at, not asserted on.

The app's own UI smoke test walks a whole shift over the in-memory repositories, which exist only where INTERNAL_TOOLS is defined — so every scheme's test action runs Beta Dev, whichever app the scheme builds. Dev because a test that signs in should not be doing it against the live platform.