- 3 worktrees:
Fun-iOS(main),Fun-iOS-NavigationStack(feature/navigation-stack),Fun-iOS-NavigationStack-Observation(feature/observation) - Always commit shared changes to main first, push, then rebase feature branches onto main
- Never make the same change independently on multiple branches
- PR discipline: Each PR's diff must only contain changes necessary for that branch's migration. Don't delete or rewrite general rules (architecture, naming, protocol placement, etc.) that still apply — only add branch-specific content on top. If a general rule needs changing, do it on the base branch first and rebase.
- Always
git pullbefore making changes or force pushing — remote may have commits from Claude bot (GitHub PR suggestions, CI fixes). Force pushing without pulling first will silently destroy those commits. After every rebase,git fetchthe remote ref before--force-with-lease. - After every rebase, diff against the base branch to verify no content was silently dropped (comments, renames, doc comments). Rebase conflict resolution with
--theirs/--ourscan lose changes from the other side. - Sync tool:
scripts/sync-branches.shor/syncin Claude Code
- When something is done repetitively (3+ times), proactively suggest automating it — as a script, skill, agent, or project rule, whichever fits best
- After adding new features or capabilities, always consider updating the project README if it's user-visible
- Production-quality Swift 6 with strict concurrency. Think about actor isolation and Sendable before writing code.
- Clarify before coding when requirements are ambiguous. Don't guess — ask.
- Test what you build. Run the full test suite after changes.
- Follow existing patterns in the codebase. Consistency > novelty.
- Workspace:
Fun.xcworkspace(required for SPM test discovery — project alone won't work) - Build:
xcodebuild build -workspace Fun.xcworkspace -scheme FunApp -destination 'platform=iOS Simulator,name=iPhone 17 Pro' CODE_SIGNING_ALLOWED=NO - Test:
xcodebuild test -workspace Fun.xcworkspace -scheme FunApp -skip-testing UITests -destination 'platform=iOS Simulator,name=iPhone 17 Pro' CODE_SIGNING_ALLOWED=NO - Lint:
swiftlint lint --quiet swift testdoes NOT work — packages are iOS-only, no macOS target
| Package dir | Import as | Library product |
|---|---|---|
Core/ |
import FunCore |
FunCore |
Model/ |
import FunModel |
FunModel |
Model/ (tests) |
import FunModelTestSupport |
FunModelTestSupport |
Services/ |
import FunServices |
FunServices |
ViewModel/ |
import FunViewModel |
FunViewModel |
UI/ |
import FunUI |
FunUI |
Coordinator/ |
import FunCoordinator |
FunCoordinator |
Coordinator → UI → ViewModel → Model → Core
Services → Model → Core
Never import upward. ViewModel must NOT import UI or Coordinator. Model must NOT import Services.
import UIKitin ViewModel or Model packages — UIKit belongs in UI and Coordinator only- Coordinator references in ViewModels (except weak optional closures) — retain cycle risk
print()anywhere — use LoggerServiceUserDefaults.standardoutside Services — use FeatureToggleService- Adding
fatalError()for missing services — ServiceLocator.resolve() already crashes withfatalErrorif a service isn't registered; don't add redundant guards - Navigation logic in Views — navigation decisions belong in Coordinators only
- Protocol definitions in Services — domain protocols go in Model, reusable abstractions in Core
- Entry point: UIKit
AppDelegate+SceneDelegate(scene-based lifecycle) - Navigation: 6 UIKit coordinators —
AppCoordinator,BaseCoordinator,LoginCoordinator,HomeCoordinator,ItemsCoordinator,SettingsCoordinator - Views: SwiftUI views embedded in UIHostingController via UIViewControllers
- Reactive: Combine (
@Published,CurrentValueSubject,.sink) - ViewModel → Coordinator: Optional closures (
onShowDetail,onShowProfile, etc.) - DI: Session-scoped ServiceLocator — no
.sharedsingleton. EachSessioncreates and owns its ownServiceLocator. On session transition, the old ServiceLocator is released with the session (no stale services).@Serviceproperty wrapper resolves viastatic subscript(_enclosingInstance:)from the enclosing type'sserviceLocator(requiresServiceLocatorProviderconformance). Coordinators and ViewModels receive the current session's ServiceLocator via constructor injection.
Consult these files for detailed guidance (not auto-loaded — read on demand):
ai-rules/general.md— Architecture deep-dive, MVVM-C patterns, DI, sessions, testingai-rules/swift-style.md— Swift 6 concurrency, naming, Combine patterns, SwiftLint rulesai-rules/ci-cd.md— GitHub Actions CI workflow patterns
- Swift 6 strict concurrency, iOS 17+
- SwiftUI + UIKit hybrid, MVVM-C with Combine
- ViewModels use closures for navigation (no coordinator protocols)
- Navigation logic ONLY in Coordinators, never in Views
- Protocol placement: Core = reusable abstractions, Model = domain-specific
- Session-scoped ServiceLocator with
@Serviceproperty wrapper — ViewModels conform toSessionProvider, storelet session: Session - Combine over NotificationCenter for reactive state
- Swift Testing framework (
import Testing,@Test,#expect,@Suite) - Each test creates its own
MockSession(fromFunModelTestSupport) — no.serializedneeded, tests run in parallel - Use
makeSession()helper to create per-test session with mocks viaMockSession(serviceLocator:), pass viasession:param - Consolidate thin init tests into a single test when they test the same concern
- Centralized mocks in
Model/Sources/ModelTestSupport/Mocks/ - Snapshot tests with swift-snapshot-testing
- Avoid polling in tests — use
Task.sleepat suspension points, never spin-loops checking state