Hard-won learnings and gotchas for working in this codebase. These are the things that cost time once and shouldn't cost it again. For testing specifics see testing.md; for the project rules see CLAUDE.md.
OnboardingViewModel.albums is [Album], but Album is defined in ImmichClient,
not OnboardingKit. A SwiftUI view that only import OnboardingKit and touches
album.id / album.name fails to compile:
property not available due to missing import of defining module 'ImmichClient'
Fix: add import ImmichClient to any view that uses Album members
(AlbumStepView.swift does). Swift does not auto-re-export a transitive
module's members.
While editing files in the app target, the editor may show
No such module 'OnboardingKit' / 'ImmichClient' / 'XCTest'. These are transient
SourceKit diagnostics that clear once a simulator build runs and resolves the
package/target linkage. The build_sim / test_sim result is the real signal —
not the live editor diagnostics.
The onboarding UI is tested hermetically via a --uitest launch-argument seam
(UITestSupport in OwnFrameApp.swift) that injects a stub API + in-memory
stores. This is the project's standard way to make a SwiftUI flow deterministically
testable — reuse it for future flows. Full details in
testing.md.
SwiftPM test targets can't run on the simulator through the app .xcodeproj
(only library products surface as schemes). Consequence:
- Package logic → host
swift test. - Anything needing the real simulator runtime (real Keychain, real-target
integration) → the app-hosted
OwnFrameTestsbundle, which links the packages.
Editing project.pbxproj by hand is brittle — prefer to avoid it.
- Adding source files to an existing target: you can often sidestep pbxproj
entirely by putting code in a file the target already compiles. The UI-test seam,
for example, was inlined into
OwnFrameApp.swiftand the UI tests were added to the existingOwnFrameUITests.swift— zero pbxproj changes. - Adding a new local Swift package (when unavoidable): wire it into 6 spots —
PBXBuildFile(one per target frameworks phase), each target'sPBXFrameworksBuildPhasefiles, each target'spackageProductDependencies, the projectpackageReferences, plus theXCLocalSwiftPackageReferenceandXCSwiftPackageProductDependencysections. .gitignoreignoresPackages/*; each local package needs an explicit allow line (e.g.!Packages/ImmichClient/,!Packages/OnboardingKit/).
- XcodeBuildMCP setup:
.mcp.jsonmust invokenpx -y xcodebuildmcp@latest mcp(themcpsubcommand). Without it the server prints usage and the client gets-32000. Run/mcpafter a fresh start to confirm the connection. preferXcodebuild: trueforbuild_sim/test_sim— the incremental builder (xcodemake) chokes on project changes. Default sim: iPad Pro 11-inch (M4) — pin it bysimulatorId, not by name. An unpinned "iPad Pro 11"" resolves to the M5, whose default iOS 26.4 runtime serves 0 StoreKit products and fails all 7StoreKitClientTests; see testing.md.- No
axe/idbinstalled → MCP UI automation is read-only (snapshot_ui/screenshot). Drive UI through XCUITest instead. cdin a Bash tool call drifts the working directory for later calls. Use(cd … && …)subshells or absolute paths, otherwise a latergit add <repo-relative-path>can fail with "pathspec did not match".
Per CLAUDE.md: Claude orchestrates and owns the verification gate; Codex implements well-scoped slices. Keep inline (do not delegate): test design for shared/concurrent state and timing, security-critical/cross-cutting code (Keychain, TLS, onboarding wiring, app entry), and any SwiftUI/UI that needs the simulator to verify. Don't create Xcode files (schemes, etc.) while a Codex job runs in the same tree — it may "tidy" untracked out-of-scope files.
Operational details:
- Codex needs
--disable-sandbox+CLANG_MODULE_CACHE_PATH=/private/tmp/...for SwiftPM (the Home module caches aren't writable in its sandbox). That's a Codex-env workaround only — Claude re-verifies unsandboxed as the real gate. - Codex does not auto-commit. Claude verifies (host
swift test+ scope review), fixes.gitignorefor any new package, then commits. - Poll a running job with
codex-agent capture <id>grepping forWorked for [0-9](don't matcherror:— that fires on the expected red test in a TDD slice).
(from CLAUDE.md — repeated here because they shape architecture)
- An iOS app cannot physically power off the display — only dim brightness to ~0.
- Brightness / idle-timer control works only in the foreground.
- API keys and MQTT credentials go to the Keychain, never UserDefaults/logs.
- TLS validation is never disabled (a valid certificate is assumed for now).
- Verify API paths against the running Immich's OpenAPI spec — don't trust old tutorials. (Done for 2.7.5; see testing.md.)