You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+25-14Lines changed: 25 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,10 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
5
5
## Overview
6
6
7
-
A MVVM-C iOS sample app that demonstrates how to use [XCoordinator](https://github.com/quickbirdstudios/XCoordinator)v2 for navigation. There is no app logic of real-world value here — the scenes (Login → Home → News/Users → Detail/About) exist purely to exercise the coordinator types (`NavigationCoordinator`, `TabBarCoordinator`, `PageCoordinator`, `SplitCoordinator`) and the transition/animation APIs. When adding examples or fixing bugs, preserve coverage of these coordinator variants rather than collapsing them.
7
+
A MVVM-C iOS sample app that demonstrates how to use [XCoordinator](https://github.com/QuickBirdEng/XCoordinator)v3 for navigation. There is no app logic of real-world value here — the scenes (Login → Home → News/Users → Detail/About) exist purely to exercise the coordinator types (`NavigationCoordinator`, `TabBarCoordinator`, `PageCoordinator`, `SplitCoordinator`) and the transition/animation APIs. When adding examples or fixing bugs, preserve coverage of these coordinator variants rather than collapsing them.
8
8
9
-
- Swift 5, iOS 13.0+, universal (iPhone + iPad).
10
-
- Dependencies are Swift Package Manager only — no Podfile / Cartfile. Resolved versions are pinned in `XCoordinator-Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`: `XCoordinator` 2.0.5, `RxSwift` 5.0.1, `Action` 4.0.1.
9
+
- Swift 5.9, iOS 16.0+, universal (iPhone + iPad). (XCoordinator 3 requires iOS 16 / Swift 5.9.)
10
+
- Dependencies are Swift Package Manager only — no Podfile / Cartfile. Resolved versions are pinned in `XCoordinator-Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`: `RxSwift` 6.x, `Action` 5.x, and `XCoordinator` pinned to a **commit revision** on the `feature/3.0.0` branch (`QuickBirdEng/XCoordinator`) because 3.0.0 is not yet tagged — see the `XCRemoteSwiftPackageReference "XCoordinator"` block in `project.pbxproj`.
11
11
- Open `XCoordinator-Example.xcodeproj` directly (there is no `.xcworkspace`).
12
12
13
13
## Build / test commands
@@ -16,43 +16,52 @@ A MVVM-C iOS sample app that demonstrates how to use [XCoordinator](https://gith
The test plan at `XCoordinator-ExampleTests/XCoordinator-Example.xctestplan`includes both `XCoordinator-ExampleTests` (unit) and `XCoordinator-ExampleUITests` (UI) targets. Code coverage is off by default in the plan.
34
+
The test plan at `XCoordinator-ExampleTests/XCoordinator-Example.xctestplan`now contains only the `XCoordinator-ExampleUITests` (UI) target, which drives the actual app. The `XCoordinator-ExampleTests` unit target's contents were copies of XCoordinator's own `Tests/XCoordinatorTests` suite (transition/animation mechanics of the library, not the example app); since the library now owns and maintains those, the duplicates were removed and the empty unit target dropped from the plan. Code coverage is off by default in the plan.
35
35
36
36
## Architecture
37
37
38
38
### Coordinator graph
39
39
40
-
`AppDelegate` instantiates `AppCoordinator().strongRouter` and calls `setRoot(for: window)`. From there everything flows through XCoordinator `Route` enums and `prepareTransition(for:)` overrides — no view controller is allocated outside a coordinator.
40
+
`SceneDelegate` holds an `AppCoordinator()` typed as `any Router<AppRoute>` and calls `setRoot(for: window)`. From there everything flows through XCoordinator `Route` enums and `prepareTransition(for:)` overrides — no view controller is allocated outside a coordinator.
41
41
42
42
The top-level structure is:
43
43
44
-
-**`AppCoordinator`** (`NavigationCoordinator<AppRoute>`) — starts on `.login`. On `.home`, it presents a `UIAlertController` that lets the user pick between `HomeTabCoordinator`, `HomeSplitCoordinator`, `HomePageCoordinator`, or a Random one of those. The chosen home coordinator's `StrongRouter<HomeRoute>` is wrapped back into `AppRoute.home(...)` and presented full-screen. This picker is the whole point of the example — it shows the same `HomeRoute` driving three different container coordinators. Keep the three variants interchangeable from `HomeRoute`'s perspective when modifying them.
44
+
-**`AppCoordinator`** (`NavigationCoordinator<AppRoute>`) — starts on `.login`. On `.home`, it presents a `UIAlertController` that lets the user pick between `HomeTabCoordinator`, `HomeSplitCoordinator`, `HomePageCoordinator`, `HomeSwiftUICoordinator`, or a Random one of those. The chosen home coordinator (an `any Router<HomeRoute>`— the coordinator instance itself) is wrapped back into `AppRoute.home(...)` and presented full-screen. This picker is the whole point of the example — it shows the same `HomeRoute` driving four different container coordinators. Keep the variants interchangeable from `HomeRoute`'s perspective when modifying them. (The `Random` action appends `HomeSwiftUICoordinator`**last**, so the UI tests that pass `--random-picker-index 0/1/2` keep landing on Tab/Split/Page.)
45
45
-**Home container coordinators** all expose `HomeRoute { case news; case userList }` and delegate to:
46
-
-`NewsCoordinator` (`NavigationCoordinator<NewsRoute>`) — news list → detail (uses a custom `.swirl` animation on iOS 10+, falls back to `.scale`).
46
+
-`NewsCoordinator` (`NavigationCoordinator<NewsRoute>`) — news list → detail (uses a custom `.swirl` animation).
47
47
-`UserListCoordinator` (`NavigationCoordinator<UserListRoute>`) — user list → user detail (`UserCoordinator`), plus `.about` which is implemented via `addChild(AboutCoordinator(rootViewController: ...))` + `.none()` rather than a transition. `AppRoute.newsDetail` deep-links through `HomePageCoordinator → HomeRoute.news → NewsRoute.newsDetail` via `Transition.multiple(.dismissAll(), .popToRoot(), deepLink(...))`.
48
48
49
+
### SwiftUI interop (the `HomeSwiftUICoordinator` container)
50
+
51
+
`HomeSwiftUICoordinator` is a **SwiftUI** home container (the fourth picker variant) that demonstrates XCoordinator 3's SwiftUI bridge in **both directions** — it is the reference for how to mix SwiftUI and UIKit coordinators here:
52
+
53
+
-**UIKit → SwiftUI (forward):** it subclasses `ViewCoordinator<HomeRoute>` and calls `super.init(body: { HomeSwiftUIView(state:) })`, which hosts the SwiftUI view in a `RoutingController` and auto-registers the coordinator, so `@Routing<HomeRoute>` inside the view resolves to it.
54
+
-**SwiftUI → UIKit (backward):**`HomeSwiftUIView` is a `TabView` whose tabs embed the existing UIKit `UserListCoordinator` and `NewsCoordinator` via `WrappedRouter { … }`.
55
+
-**Route-driven SwiftUI state:** tab selection is a `Binding` whose setter calls `await router.trigger(.news/.userList)`; `HomeSwiftUICoordinator.prepareTransition` handles those with `Transition.withAnimation { state.selection = … }` (a no-UIKit-transition route that animates SwiftUI state).
56
+
- The `@Routing`/`Transition.withAnimation` etc. live in `Sources/XCoordinator/SwiftUI/` of the library. Deferred (not yet shown here): `redirect(to:map:)`, the `.triggerOnAppear`/`.trigger(when:)` modifiers.
57
+
49
58
### MVVM-C scene wiring
50
59
51
60
Every scene under `Scenes/<Feature>/` is three files:
52
61
53
62
-`<Feature>ViewController.swift` — `.xib`-based, conforms to `BindableType` (`Utils/BindableType.swift`). Coordinators call `VC.instantiateFromNib()` then `vc.bind(to: viewModel)`, which assigns the model, loads the view, and invokes `bindViewModel()`.
54
63
-`<Feature>ViewModel.swift` — protocol triplet: `<Feature>ViewModelInput`, `<Feature>ViewModelOutput`, and `<Feature>ViewModel { var input; var output }`. A `where Self: Input & Output` extension lets a single impl class satisfy all three by returning `self`.
55
-
-`<Feature>ViewModelImpl.swift` — concrete RxSwift implementation. Triggers are `CocoaAction`s that call `router.rx.trigger(.someRoute)` (via `XCoordinatorRx` + `Action`). Routers are held as `UnownedRouter<…>` to avoid retain cycles. Follow this pattern verbatim when adding a scene.
64
+
-`<Feature>ViewModelImpl.swift` — concrete RxSwift implementation, annotated `@MainActor` (XCoordinator 3's API is `@MainActor`, so the router-triggering closures must be too). Triggers are `CocoaAction`s that call `router.rx.trigger(.someRoute)` (via `XCoordinatorRx` + `Action`; `import XCoordinatorRx` is required for `.rx`). Routers are held as `unowned let router: any Router<…>` to avoid retain cycles. Follow this pattern verbatim when adding a scene.
56
65
57
66
### Models, services, common
58
67
@@ -62,12 +71,14 @@ Every scene under `Scenes/<Feature>/` is three files:
62
71
63
72
### Animations and transitions
64
73
65
-
-`Animations/Animation+*.swift` — custom XCoordinator `Animation` instances (`.fade`, `.scale`, `.swirl`, `.modal`, `.navigation`) built from `StaticTransitionAnimation` / `TransitionAnimation`. `AnimationTests` instantiates each coordinator type and asserts that pushing/popping triggers the configured animation's `performAnimation` blocks — when you add a new custom animation, mirror the existing test pattern.
74
+
-`Animations/Animation+*.swift` — custom XCoordinator `Animation` instances (`.fade`, `.scale`, `.swirl`, `.modal`, `.navigation`) built from `StaticTransitionAnimation` / `TransitionAnimation`. The library's own `Tests/XCoordinatorTests` (e.g. `AnimationTests`) covers that pushing/popping triggers a configured animation's blocks; this example no longer duplicates that. The example's `XCoordinator-ExampleUITests` instead verify the real app flows end-to-end (login → picker → each home container, deep links).
75
+
-**`UIHostingController` gotcha:** a custom animator must give the incoming view a real frame. All five animations set `toView.frame = transitionContext.finalFrame(for:)` (+ `autoresizingMask`) before animating — without it, UIKit does not pre-size the presented view for a custom animator and a `UIHostingController`'s SwiftUI view lays out to zero and renders **blank/black** (this is exactly what happened to `HomeSwiftUICoordinator` under `.fade`). Keep this pattern in any new custom animator.
66
76
-`Extensions/Transitions.swift` — defines two app-specific transition factories: `Transition.presentFullScreen(_:animation:)` (sets `modalPresentationStyle = .fullScreen` before `.present`) and `Transition.dismissAll()` (recursively dismisses presented VCs). Prefer these over inline `modalPresentationStyle` mutation.
67
77
-`Extensions/TransitionAnimation+Defaults.swift` — shared defaults reused across the custom animations.
68
78
69
79
### Conventions worth keeping
70
80
71
81
- Routes are always enums conforming to `Route`. Adding a new screen means: add a case to the relevant `…Route`, handle it in the matching coordinator's `prepareTransition`, and add a scene triplet under `Scenes/`.
72
-
- Coordinators expose themselves to view models as `unownedRouter` (or `strongRouter` when handed across coordinator boundaries, as in `AppRoute.home`). Do not pass coordinators directly into view models.
82
+
-`prepareTransition(for:)` overrides use XCoordinator 3's `@TransitionBuilder` (inherited from `BaseCoordinator`): each `switch` case is a single trailing `Transition` expression with no `return`. The builder transforms reliably only when each case is **one** expression — a case body with `void` statements, local `let`s, `if/else`, or several listed transitions falls back to "missing return". So view-controller construction (instantiate + `bind(to:)`), `addChild`, control flow, and transition chains (`.multiple(...)`) live in `private func` helpers that each return a `Transition`, keeping every case a single expression. Keep this shape when adding scenes.
83
+
- Coordinators pass themselves to view models as `self` (a coordinator is an `any Router<…>`); the view model stores it `unowned`. When a router is handed across coordinator boundaries (as in `AppRoute.home`) the coordinator instance is held strongly as `any Router<…>`. Do not pass coordinators into view models by concrete type.
73
84
- Child coordinators that share a navigation stack with their parent (see `AboutCoordinator` in `UserListCoordinator`) are attached with `addChild(...)` + a `.none()` transition rather than a presentation.
0 commit comments