SwiftUI
Swift 6 strict concurrency, observable repositories over SwiftData, hand-built composition through the environment, generated OpenAPI clients, and native controls the compiler refuses outside the design system.
Verified 2026-10-03 · Against a production SwiftUI super-app: Swift 6.0 with complete strict concurrency, iOS 18 minimum, Xcode 26, swift-openapi-runtime 1.12 (the example: Swift 6.4, Xcode 27), Firebase 12.19, Nuke 13.2. Excerpts are adapted from that codebase, with names changed. Blocks titled
examples/mobile/ios/…are that example in this handbook: a Swift package its gate builds and tests withswift teston a Mac, no simulator. The rules both apps share are on native mobile; this page is what is particular to iOS.
Libraries and versions
| Concern | Choice | Version |
|---|---|---|
| Language | Swift, SWIFT_STRICT_CONCURRENCY = complete | 6.0 |
| UI | SwiftUI, Observation | iOS 18 |
| Local storage | SwiftData | iOS 18 |
| Secrets | Keychain | — |
| Packages | Swift Package Manager only, resolved file honoured | — |
| API clients | Swift OpenAPI Generator output, swift-openapi-runtime, swift-http-types | 1.12.1, 1.8.0 |
| Images | Nuke, NukeUI | 13.2.0 |
| Push, attestation, analytics, crashes, performance | Firebase (Messaging, AppCheck, Analytics, Crashlytics, Performance) | 12.19.1 |
| Phone numbers | PhoneNumberKit | 5.0.8 |
| Social sign-in | GoogleSignIn, Facebook Login (exact pins) | 9.0.0, 18.1.0 |
| Logging | OSLog | — |
Native first, packages when they bring clear value — a mature image cache, a vendor SDK. Name the reason in the commit that adds one.
Never: CocoaPods or Carthage; a dependency-injection framework;
ObservableObject and @Published in new code; AsyncImage; the iOS Simulator.
Composition through the environment
MainApp.init() builds everything by hand, in tiers, with no DI framework.
Dependencies reach views through SwiftUI's environment.
// Before any request is built: every request to the platform names this build and its API
// version, and a must-update reply is remembered for this build (the cover opens at launch).
let updateRefusals = UpdateRefusalMemory(partition: StorageScope.current.key,
build: AppAPIVersion.builtApp,
apiVersion: AppAPIVersion.builtVersion)
RequestPolicy.install(.app(refusals: updateRefusals))
// Tier 1 — Infrastructure
let tokenStore = TokenStore(storefront: AppConfig.storefrontClient)
let identitySession = IdentitySession(tokenStore: tokenStore,
appCheck: FirebaseAppCheckTokenProvider(),
baseURL: AppConfig.apiBaseURL)
// Tier 2 — Remote sources (home tier only)
let eventsRemote = EventsRemoteSource(client: apiClient)
// Tier 3 — Home-tier repositories (eager, needed at launch)
let context = ModelContext(modelContainer)
eventsRepository = EventsRepository(modelContext: context, remoteSource: eventsRemote)
// Every other repository, built on first use and kept for the session
repositoryProvider = RepositoryProvider(modelContext: context, apiClient: apiClient,
identitySession: identitySession)Adapted from a production app's init(), shortened; the real root wires about
sixty stores the same way and attaches each with .environment(store) in body.
The example's root is the same shape, small enough to read whole:
import Foundation
import Observation
import SwiftData
import SwiftUI
/// The composition root, built by hand in tiers, in an order that matters. No DI framework: each
/// tier takes what the one above built, and the root view receives the stores through the environment.
@MainActor
final class AppComposition {
let policy: RequestPolicy
let standing: BuildStanding
let projects: ProjectsRepository
let links = PendingDeepLinkStore()
let container: ModelContainer
/// `exchange` is the identity endpoints' attested client, which the app target supplies.
init(config: AppConfig, exchange: IdentityExchange, workspaceID: String, inMemory: Bool = false) throws {
// Before any request is built: the outbound rule, with this build's remembered refusal.
let memory = DefaultsUpdateRefusalMemory()
let standing = BuildStanding(refused: memory.isRefused(build: config.appVersion))
self.standing = standing
policy = RequestPolicy(config: config, memory: memory) { refused in
Task { @MainActor in standing.refused = refused }
}
// Tier 1 — infrastructure
let session = IdentitySession(tokens: KeychainTokenStore(service: "projects.session"), exchange: exchange)
let client = APIClient(policy: policy, transport: URLSessionTransport(policy: policy), credentials: session)
// Tier 2 — remote sources
let remote = ProjectsRemoteSource(api: ProjectsAPI(baseURL: config.apiBaseURL, client: client), workspaceID: workspaceID)
// Tier 3 — repositories: the home's now; any other is built on first use and kept
container = try ModelContainer(for: ProjectRecord.self, configurations: ModelConfiguration(isStoredInMemoryOnly: inMemory))
projects = ProjectsRepository(local: SwiftDataProjectsStore(context: container.mainContext), remote: remote)
}
/// The root view with every store in the environment. The app target's `@main` shows it in its scene.
func rootView() -> some View {
AppShell()
.environment(projects)
.environment(links)
.environment(standing)
.modelContainer(container)
}
}
/// Whether this build is refused, for the must-update cover.
@Observable @MainActor
final class BuildStanding {
var refused: Bool
init(refused: Bool) { self.refused = refused }
}
- Views read with
@Environment(ProjectsRepository.self). - Locale-dependent values are attached in
RootView, not inbodyof theApp, or a language switch leaves stale strings. - Account changes fan out from one place: the identity store's
onAccountChangere-binds every private store, so there is one list to audit.
State
- Repositories and stores are
@Observable @MainActor final class. - Infrastructure that owns mutable shared state is an
actor: the token store, the identity session, the push registrar. - Work is structured:
.taskand.task(id:)on views,scenePhasechanges at the root. No detached tasks for screen work, no timers of a feature's own.
Offline-first repository
One generic base handles the common case: a single collection, read from SwiftData first, refreshed from the server, upserted, read again.
/// Generic offline-first repository for a single collection of upsertable models.
/// Subclasses override `sortDescriptors`, `localPredicate` and `fetchRemote()`, and
/// inherit `items`, `isLoading`, `error`, `hasLoaded`, `loadIfNeeded()`,
/// `fetchLocal()`, `refresh()` and `refreshIfNeeded()`.
@Observable
@MainActor
class RepositoryBase<Item: SwiftDataUpsertable> {
let modelContext: ModelContext
private(set) var items: [Item] = []
private(set) var isLoading = false
private(set) var error: Error?
private(set) var hasLoaded = false
/// When the collection last arrived from the server, across launches.
let freshness: ContentFreshness
var sortDescriptors: [SortDescriptor<Item>] { [] }
var localPredicate: Predicate<Item>? { nil }
func fetchRemote() async throws -> [Item] { throw AppError.network(message: "fetchRemote not implemented") }
}Adapted from a production app. Every store, whatever its shape, conforms to one
LoadStatus protocol, so a screen asks the same question of a repository, a pager
or a rail.
When not to: a repository that owns several entities composes several bases or extends one; it does not grow a second loading model.
The example keeps storage behind a port, so the repository's rules — cached first, a failure keeps content — are tested against real SwiftData in memory:
import Foundation
import Observation
/// The projects this phone holds. A screen draws them at once from storage, then the repository
/// refreshes in place; a failed refresh keeps them and says so through `LoadStatus`.
@Observable @MainActor
final class ProjectsRepository: LoadStatus {
private(set) var projects: [Project] = []
private(set) var isLoading = false
private(set) var hasLoaded = false
private(set) var error: AppError?
@ObservationIgnored private let local: ProjectsLocalStore
@ObservationIgnored private let remote: ProjectsRemoteSource
@ObservationIgnored private var hasRead = false
init(local: ProjectsLocalStore, remote: ProjectsRemoteSource) {
self.local = local
self.remote = remote
}
var phase: LoadPhase { phase(hasContent: !projects.isEmpty) }
var isStale: Bool { isStale(hasContent: !projects.isEmpty) }
/// What storage holds, on first access. Safe to call on every appearance.
func loadIfNeeded() {
guard !hasRead else { return }
hasRead = true
projects = local.projects()
}
/// Reads the server's list and keeps it. A failure keeps what is on screen.
func refresh() async {
loadIfNeeded()
isLoading = true
defer {
isLoading = false
hasLoaded = true
}
do {
try local.replace(with: try await remote.projects())
projects = local.projects()
error = nil
} catch {
self.error = (error as? AppError) ?? .decoding
}
}
func project(id: String) -> Project? { projects.first { $0.id == id } }
}
import Foundation
/// The facts every store reports about the request behind what it holds, so a screen asks one
/// question of any of them. `LoadPhase` turns them, with whether anything is cached, into what a
/// surface draws. Android's `LoadStatus` is the same.
@MainActor
protocol LoadStatus: AnyObject {
/// A request is in flight.
var isLoading: Bool { get }
/// A request completed since launch, whatever its outcome.
var hasLoaded: Bool { get }
/// The last request failed and nothing succeeded since.
var error: AppError? { get }
}
extension LoadStatus {
func phase(hasContent: Bool) -> LoadPhase {
LoadPhase.of(hasContent: hasContent, isLoading: isLoading, hasLoaded: hasLoaded, failed: error != nil)
}
/// Cached content is on screen because the request that would have replaced it failed.
func isStale(hasContent: Bool) -> Bool { hasContent && error != nil }
}
/// What a surface has to draw. Cached content always wins: while there is something to show, a
/// refresh in flight and a refresh that failed are both silent here, and the failure reaches the
/// person once, through the screen's stale notice.
enum LoadPhase: Equatable, Sendable {
/// Something to draw, however old.
case content
/// Nothing cached and the first answer is still to come: draw the skeleton, never a spinner.
case loading
/// Nothing cached and the request failed: a retry stands where the content would.
case failed
/// Nothing cached and the server said so.
case empty
static func of(hasContent: Bool, isLoading: Bool, hasLoaded: Bool, failed: Bool) -> LoadPhase {
if hasContent { return .content }
if isLoading || !hasLoaded { return .loading }
return failed ? .failed : .empty
}
}
A feature slice
Models/Project.swift # the Project value, and ProjectRecord: the only type that knows SwiftData
Services/ProjectsAPI.swift # stands in for the generated client
Services/ProjectsRemoteSource.swift # the generated client's reply, mapped onto Project
Repositories/ProjectsLocalStore.swift # the storage port and its SwiftData implementation
Repositories/ProjectsRepository.swift # @Observable @MainActor, LoadStatus
Components/ProjectCard.swift # composed from design-system pieces only
ProjectsView.swift
ProjectDetailView.swiftThe remote source calls the generated client
// Stands in for the Swift client generated from the Catalog service's OpenAPI document: in a real
// project this file is generated (Swift OpenAPI Generator), pinned to the platform revision Android
// pins, and never edited. Its shapes are the Go example's `ProjectResponse` and `ProjectListResponse`.
import Foundation
struct ProjectsAPI: Sendable {
struct ProjectResponse: Decodable, Sendable {
let id: String
let name: String
let country: String
let status: String
let startDate: String?
let endDate: String?
let createdAt: String
let updatedAt: String
}
struct ProjectListResponse: Decodable, Sendable {
let items: [ProjectResponse]
let total: Int
}
let baseURL: URL
let client: APIClient
/// `GET /api/catalog/workspaces/{workspaceId}/projects`
func listProjects(workspaceID: String, limit: Int = 100) async throws -> ProjectListResponse {
var url = baseURL.appending(path: "api/catalog/workspaces/\(workspaceID)/projects")
url.append(queryItems: [URLQueryItem(name: "limit", value: String(limit))])
let data = try await client.send(URLRequest(url: url))
do {
return try JSONDecoder().decode(ProjectListResponse.self, from: data)
} catch {
throw AppError.decoding
}
}
}
import Foundation
/// Projects from the Catalog service, through its generated client, mapped onto `Project`.
/// A reply the app cannot read is a decoding failure, never a partial project.
struct ProjectsRemoteSource: Sendable {
let api: ProjectsAPI
let workspaceID: String
func projects() async throws -> [Project] {
try await api.listProjects(workspaceID: workspaceID).items.map { reply in
guard let updatedAt = ISO8601DateFormatter().date(from: reply.updatedAt) else { throw AppError.decoding }
return Project(id: reply.id, name: reply.name, country: reply.country,
status: Project.Status(rawValue: reply.status) ?? .active, updatedAt: updatedAt)
}
}
}
The APIClient is the only way a request leaves, so every generated call goes through the
one outbound rule.
- The generated package is pinned by revision in the project's package list, the same platform revision Android pins.
API-Versionis read in one place, from the generated runtime package.
The screen only composes
import SwiftUI
/// The projects page: what the phone holds at once, refreshed in place.
struct ProjectsView: View {
@Environment(ProjectsRepository.self) private var repository
@Environment(\.translations) private var t
var body: some View {
PageLayout(title: t("projects.title")) {
PageSection(title: t("projects.section.all")) {
LoadedSection(phase: repository.phase, isStale: repository.isStale,
emptyMessage: t("projects.empty"),
failedMessage: repository.isStale ? t("projects.stale") : t("projects.failed"),
retryTitle: t("projects.retry"),
retry: { Task { await repository.refresh() } }) {
ForEach(repository.projects) { project in
AppLink(route: .project(id: project.id)) { ProjectCard(project: project) }
}
}
}
}
.task {
repository.loadIfNeeded()
await repository.refresh()
}
}
}
The compiler refuses native controls
Inside the app module, an unqualified native control does not compile. The
design system spells the native type as SwiftUI.Button; a feature screen gets an
error naming the shared component.
import SwiftUI
// Compile-time ownership guard.
//
// Inside this module an unqualified `Button`, `ScrollView`, `NavigationStack`, `List` or `TextField`
// resolves to one of these unavailable aliases, so a feature screen cannot hand-roll a control or a
// page container by accident: the compiler names the shared component instead. The design system
// spells the native type as `SwiftUI.Button`. `tooling/check.py` covers what the compiler cannot:
// module-qualified names, styling modifiers, native presentation and requests outside Core/Network.
@available(*, unavailable, renamed: "AppButton", message: "Feature screens compose AppButton; the design system spells SwiftUI.Button.")
typealias Button<Label: View> = SwiftUI.Button<Label>
@available(*, unavailable, renamed: "PageLayout", message: "Use PageLayout for a page; the design system spells SwiftUI.ScrollView.")
typealias ScrollView<Content: View> = SwiftUI.ScrollView<Content>
@available(*, unavailable, renamed: "AppNavigationStack", message: "Use AppNavigationStack, once, at the shell; the design system spells SwiftUI.NavigationStack.")
typealias NavigationStack<Data, Root: View> = SwiftUI.NavigationStack<Data, Root>
@available(*, unavailable, renamed: "PageSection", message: "Use PageLayout with PageSection; the design system spells SwiftUI.List.")
typealias List<SelectionValue: Hashable, Content: View> = SwiftUI.List<SelectionValue, Content>
@available(*, unavailable, renamed: "AppTextInput", message: "Use AppTextInput; the design system spells SwiftUI.TextField.")
typealias TextField<Label: View> = SwiftUI.TextField<Label>
The production app's file also covers secure fields, text editors, toggles, pickers, date pickers, sliders, steppers, forms, tab views and grids.
The compiler cannot see everything, so a script covers the rest: module-qualified
names outside the design system, styling modifiers (.font, .background,
.shadow…), Color(, shapes, padding that is not a spacing token, .sheet,
.alert, .toolbar, .refreshable, UIKit containers, and any request that does not
go through the outbound rule.
#!/usr/bin/env python3
"""The SwiftUI rules the compiler cannot enforce, refused in every Swift file outside the design system.
python3 tooling/check.py [--root DIR]
A file whose path has a `DesignSystem` folder owns the native controls, styling and presentation; every
other file composes it. Requests leave only from `Core`, through the API client and the request rule. Tests, build output and package
checkouts are not scanned. A literal that is a translation key ("projects.title") is not words.
There is no allowlist: a rule that does not fit is changed here, with its
reason, never skipped in a file.
"""
import argparse
import re
import sys
from pathlib import Path
SKIP = {".build", ".swiftpm", ".git", "Pods", "DerivedData", "node_modules", "tooling"}
# (rule, pattern, what to do instead). Matched on code with comments removed and string contents blanked.
OUTSIDE_DESIGN_SYSTEM = [
("native-control", r"\bSwiftUI\.\w+|\b(NavigationLink|Text|Image|Label|Menu|Link|Toggle|Picker)\s*\(",
"compose a design-system component (AppText, AppButton, AppLink…)"),
("styling", r"\.(font|foregroundStyle|foregroundColor|background|tint|cornerRadius|clipShape|shadow|border|"
r"opacity|buttonStyle)\s*\(",
"styling belongs to the design system: use or extend one of its components"),
("paint", r"\bColor\s*\(|\bColor\.(red|green|blue|black|white|gray|orange|yellow|pink|purple)\b|"
r"\b(RoundedRectangle|Circle|Capsule|LinearGradient|RadialGradient)\s*\(",
"use AppColors and the design system's shapes"),
("literal-size", r"\.padding\((?:\.\w+\s*,\s*)?[1-9][\d.]*\s*\)|\bspacing:\s*[1-9]|"
r"\.frame\([^)]*\b(width|height|minHeight|maxHeight|minWidth|maxWidth):\s*[1-9]",
"use an AppSpacing token"),
("presentation", r"\.(sheet|fullScreenCover|popover|alert|confirmationDialog)\s*\(",
"present through the design system (appSheet…)"),
("spinner", r"\bProgressView\s*\(", "a first load draws LoadedSection's skeleton, never a spinner"),
("image-loader", r"\bAsyncImage\b", "use the design system's one image loader"),
("literal-words", r'\bAppText\(\s*""|\bAppButton\(\s*title:\s*""',
"every word is translated: read it from Translations"),
]
EVERYWHERE = [
("observable-object", r"\bObservableObject\b|@Published\b", "use @Observable"),
]
NETWORK = ("network-door", r"\bURLSession\b", "requests leave only through Core (the API client and the request rule)")
STRING = re.compile(r'"(?:[^"\\\n]|\\.)*"')
KEY = re.compile(r'"[a-z0-9_]+(\.[a-z0-9_]+)+"')
def blank(literal):
"""A string's contents, blanked to what the rules tell apart: a translation key ("projects.title") or
a mark with no letter ("·") is not words, a string that interpolates ("\\(count) left") is composed,
anything else is words."""
if KEY.fullmatch(literal) or not re.search(r"[^\W\d_]", literal):
return '"key"'
return '"\\("' if "\\(" in literal else '""'
def plain_code(line):
"""The line with string contents blanked and its comment removed."""
return re.sub(r"//.*$", "", STRING.sub(lambda m: blank(m.group(0)), line))
def swift_files(root):
for path in sorted(root.rglob("*.swift")):
parts = path.relative_to(root).parts
if any(p in SKIP or p.endswith("Tests") or p == "Tests" for p in parts[:-1]) or path.name == "Package.swift":
continue
yield path
def violations(root):
root = Path(root).resolve()
out = []
for path in swift_files(root):
rel = path.relative_to(root).as_posix()
parts = path.relative_to(root).parts[:-1]
in_design_system = "DesignSystem" in parts
in_core = "Core" in parts
for number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
plain = plain_code(raw)
rules = list(EVERYWHERE) + ([] if in_design_system else OUTSIDE_DESIGN_SYSTEM)
rules += [] if in_core else [NETWORK]
for rule, pattern, instead in rules:
if re.search(pattern, plain):
out.append(f"{rel}:{number}: {rule}: {instead}")
return out
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--root", default=".", help="the folder to scan (default: here)")
args = parser.parse_args(argv)
found = violations(args.root)
for line in found:
print(line)
if found:
print(f"❌ {len(found)} SwiftUI rule violation(s)")
return 1
print("✅ SwiftUI rules hold")
return 0
if __name__ == "__main__":
sys.exit(main())
The rules have their own tests: each plants a violation in a throwaway tree and expects a refusal, and the example itself must pass.
Apple first
Use the platform's own chrome as Apple ships it: the native tab bar, navigation bars and materials. Do not rebuild them or nudge them by points.
The production app reverted three tab-bar tweaks in one day: a 2 pt adjustment broke the iOS 26 Liquid Glass baseline. Custom bottom bars were later deleted outright.
Bottom sheets are the one deliberate departure: plain, never glass, so their content stays legible.
Identity and attestation
- The session token lives in the Keychain as
AfterFirstUnlockThisDeviceOnly; a biometric copy uses.biometryCurrentSet. - App Attest in every build, with the entitlement's environment set to
productioneven for development builds: Firebase refuses attestations from Apple's sandbox. The debug provider is not used, because builds run on devices.
Proof on the Mac
Tests run on the Mac with swift test, never on a simulator. The app's code lives in a
Swift package (or a framework target) that builds for macOS as well as iOS, so XCTest runs
it directly. --disable-sandbox is needed only inside another sandbox, such as an agent's,
where SwiftPM's own cannot nest.
# The handbook's SwiftUI example: built and tested on this Mac, never on a simulator.
.DEFAULT_GOAL := check
.PHONY: check test rules rules-tests
check: test rules rules-tests ## the tests, then every SwiftUI rule, then the rules' own tests
test: ## The tests, on the Mac (--disable-sandbox: SwiftPM's own sandbox cannot nest inside another)
swift test --disable-sandbox
rules: ## What the compiler cannot refuse: styling, presentation, words and requests outside their owners
@python3 tooling/check.py --root .
rules-tests: ## The rules themselves fail on planted violations
@python3 tooling/test_check.py
import SwiftData
import XCTest
@testable import ProjectsApp
@MainActor
final class ProjectsRepositoryTests: XCTestCase {
private var container: ModelContainer!
override func setUp() async throws {
container = try ModelContainer(for: ProjectRecord.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
}
private func repository(_ transport: ScriptedTransport) -> ProjectsRepository {
let client = APIClient(policy: RequestPolicy(config: config, memory: MemoryRefusals()), transport: transport,
credentials: StaticCredentials(), pause: { _ in })
let remote = ProjectsRemoteSource(api: ProjectsAPI(baseURL: platform, client: client), workspaceID: "w-1")
return ProjectsRepository(local: SwiftDataProjectsStore(context: container.mainContext), remote: remote)
}
func test_a_first_visit_draws_the_skeleton_then_the_projects() async {
let repository = repository(ScriptedTransport([.reply(200, projectsReply(["Bangui water"]))]))
repository.loadIfNeeded()
XCTAssertEqual(repository.phase, .loading)
await repository.refresh()
XCTAssertEqual(repository.phase, .content)
XCTAssertEqual(repository.projects.map(\.name), ["Bangui water"])
}
func test_a_returning_visit_draws_the_kept_projects_before_any_answer() async {
await repository(ScriptedTransport([.reply(200, projectsReply(["Bangui water", "Bimbo school"]))])).refresh()
let next = repository(ScriptedTransport([]))
next.loadIfNeeded()
XCTAssertEqual(next.phase, .content)
XCTAssertEqual(Set(next.projects.map(\.name)), ["Bangui water", "Bimbo school"])
}
func test_a_failed_refresh_keeps_the_projects_and_says_they_are_stale() async {
await repository(ScriptedTransport([.reply(200, projectsReply(["Bangui water"]))])).refresh()
let next = repository(ScriptedTransport([.noAnswer, .noAnswer, .noAnswer]))
await next.refresh()
XCTAssertEqual(next.phase, .content)
XCTAssertTrue(next.isStale)
XCTAssertEqual(next.error?.failure, .noConnection)
XCTAssertEqual(next.projects.map(\.name), ["Bangui water"])
}
func test_nothing_kept_and_a_failure_offers_a_retry() async {
let repository = repository(ScriptedTransport([.reply(500, "{}"), .reply(500, "{}"), .reply(500, "{}")]))
await repository.refresh()
XCTAssertEqual(repository.phase, .failed)
}
func test_the_server_saying_none_is_empty_not_failed() async {
let repository = repository(ScriptedTransport([.reply(200, projectsReply([]))]))
await repository.refresh()
XCTAssertEqual(repository.phase, .empty)
}
func test_a_reply_the_app_cannot_read_keeps_what_was_kept() async {
await repository(ScriptedTransport([.reply(200, projectsReply(["Bangui water"]))])).refresh()
let next = repository(ScriptedTransport([.reply(200, #"{"items":[{"id":"x"}]}"#)]))
await next.refresh()
XCTAssertEqual(next.error, .decoding)
XCTAssertEqual(next.projects.map(\.name), ["Bangui water"])
}
}
A transport that answers from a script stands in for the network; everything above it — the request rule, credentials, retries, decoding, storage — is the real code.
When not to: an app whose code cannot build for macOS (UIKit-only APIs throughout) keeps
the logic in a package that can, or compiles the needed sources with swiftc and a small
fixture program, as the production app does.