Claude Code instructions for SwiftIntro
April 16, 2026 · View on GitHub
Architecture details, design decisions, and type reference: DESIGN.md
Project overview
UIKit memory card game using Mobius unidirectional data flow, Factory DI, and Kingfisher image loading. Targets iOS 17+, Xcode 26.1, Swift 6.
Architecture rules — always follow these
Mobius loop
- All game state lives in
GameModel. Never store state in a view or view controller. GameLogic.updatemust stay pure — no UIKit, no side effects, no network calls.- Effects (
GameEffect) are the only way to trigger animations, timers, and navigation. - Score and other model-derivable values must be rendered from
GameModelinrender(_:), never via a dedicated effect. - The loop infrastructure (
MobiusController+GameEffectHandler) is owned byGameLoop, not the VC. GameVCis a pure view — it implementsConnectablebut has no direct reference toMobiusControllerorGameEffectHandler.- Create the loop via
Mobius.loop(...).makeController(from:)—MobiusController.initis internal.
View / VC split
- All layout and subviews belong in a
*View.swiftfile, never in the view controller. - Every VC must override
loadView()and assign the custom view class — noview.addSubview(...)inviewDidLoad. - View controllers handle:
loadView(), lifecycle hooks, navigation, and wiring closures. Nothing else. - Target < 60 lines per VC file.
Dependency injection
- Use
@Injected(\.keyPath)property wrappers — never pass dependencies through constructors unless they vary per instance (e.g.config,cards). - Consumers reference protocols (
WikimediaClientProtocol,HTTPClientProtocol,ImageCacheProtocol), never concrete types. - Register new services as
.singletoninDependencyInjection+Registration.swift.
Networking
- No
async/await— use closure callbacks (done: @escaping (Result<T, Error>) -> Void). - Completion closures are called on an arbitrary background queue; always dispatch UI updates via
DispatchQueue.main.async { }. - Wikimedia-specific decoding types stay
privateinsideWikimediaClient.swift.
Naming
CardSingles— unique cards from the API, no duplicates.CardDuplicates— shuffled paired deck ready for play.- Never use the bare name
Cards.
Code style
finalon all classes that are not designed for subclassing.- No
NSObjectinheritance unless required by an@objcprotocol (onlyMemoryDataSourceAndDelegateneeds it). - Prefer
private extension TypeName { }for grouping private methods over// MARK: - Privatewith mixed access. - Document every public/internal symbol with
///doc comments. Non-trivial inline logic gets//comments. - No storyboards or XIBs — all UI is programmatic.
File organisation
Group files by feature, not by type:
Features/Game/Logic/ — GameModel, GameEvent, GameEffect, GameLogic, GameEffectHandler
Features/Game/View/ — GameView, GameHeaderView, CardCVCell, MemoryDataSourceAndDelegate
Features/Game/ — GameVC
Features/Settings/ — SettingsVC, SettingsView, GameConfiguration
Features/Loading/ — LoadingDataVC, LoadingView
Features/GameOver/ — GameOverVC, GameOverView, GameOutcome
Models/ — Card, CardSingles, CardDuplicates, Level
Networking/ — HTTPClient(Protocol), APIClient(Protocol), Router, ImagePrefetcher
Views/ — Shared reusable views (CircularButton, CellProtocol)
SupportingFiles/ — AppDelegate, SceneDelegate, Container+SwiftIntro, Logger, Extensions
Every new file must be added to SwiftIntro.xcodeproj/project.pbxproj.
Session setup — always do this first
- Run
scripts/setup.sh(or manuallypip install pre-commit && pre-commit install) to install git hooks and, on macOS, theswiftformat,swiftlint,just, andxcprettybinaries. The script is idempotent. - Run
pre-commit run --all-filesbefore pushing to catch lint/format issues early. SwiftFormat/SwiftLint/tests skip gracefully when their binaries aren't on$PATH(Linux sandbox), so always verify against CI results in that case. - Never use
git commit --no-verify. If a hook blocks a commit, fix the underlying issue.
Key gotchas
-
UIViewController is
@MainActorin the Xcode 26 SDK. This makes the Swift compiler stricter about implicitselfin escaping closures inside VC subclasses than in plain classes. Useguard let self else { return }+ explicitself.propertyand wrap with// swiftformat:disable redundantSelf/// swiftformat:enable redundantSelfso SwiftFormat doesn't strip theself.that the compiler requires. -
configureCellis called fromwillDisplay, notcellForItemAt— this ensures Kingfisher is invoked every time a cell re-enters the visible area, not only on first dequeue. -
GameEffectHandler.currentModelis pre-seeded withinitialModelat init time. Do not change this tonil— the Mobius loop delivers the first model asynchronously and cells would appear blank otherwise. -
MobiusController.stop()anddisconnectView()must both be called inviewDidDisappearto cancel pendingDispatchWorkItemtimers and avoid delivering events to a detached loop. -
DispatchWorkItemfor the flip-back timer is stored inflipBackWorkItemso it can be cancelled when the loop stops. -
The
WikimediaResponseDecodable types areprivatetoWikimediaClient.swift— do not make them internal or public.