Skip to content

Commit fab1cfb

Browse files
Update to XCoordinator 3.0.0
1 parent 8f24213 commit fab1cfb

35 files changed

Lines changed: 485 additions & 590 deletions

CLAUDE.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Overview
66

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.
88

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`.
1111
- Open `XCoordinator-Example.xcodeproj` directly (there is no `.xcworkspace`).
1212

1313
## Build / test commands
@@ -16,43 +16,52 @@ A MVVM-C iOS sample app that demonstrates how to use [XCoordinator](https://gith
1616
# Build for the simulator
1717
xcodebuild -project XCoordinator-Example.xcodeproj \
1818
-scheme XCoordinator-Example \
19-
-destination 'platform=iOS Simulator,name=iPhone 15' \
19+
-destination 'platform=iOS Simulator,name=iPhone 17' \
2020
build
2121

22-
# Run the full test plan (unit + UI tests)
22+
# Run the full test plan (UI tests)
2323
xcodebuild -project XCoordinator-Example.xcodeproj \
2424
-scheme XCoordinator-Example \
2525
-testPlan XCoordinator-Example \
26-
-destination 'platform=iOS Simulator,name=iPhone 15' \
26+
-destination 'platform=iOS Simulator,name=iPhone 17' \
2727
test
2828

2929
# Run a single test (Xcode test identifier: Target/Class/method)
3030
xcodebuild ... test \
31-
-only-testing:XCoordinator-ExampleTests/AnimationTests/testPageCoordinator
31+
-only-testing:XCoordinator-ExampleUITests/XCoordinator_ExampleUITests/testTabPickerLandsOnTabBar
3232
```
3333

34-
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.
3535

3636
## Architecture
3737

3838
### Coordinator graph
3939

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.
4141

4242
The top-level structure is:
4343

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.)
4545
- **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).
4747
- `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(...))`.
4848

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+
4958
### MVVM-C scene wiring
5059

5160
Every scene under `Scenes/<Feature>/` is three files:
5261

5362
- `<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()`.
5463
- `<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.
5665

5766
### Models, services, common
5867

@@ -62,12 +71,14 @@ Every scene under `Scenes/<Feature>/` is three files:
6271

6372
### Animations and transitions
6473

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.
6676
- `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.
6777
- `Extensions/TransitionAnimation+Defaults.swift` — shared defaults reused across the custom animations.
6878

6979
### Conventions worth keeping
7080

7181
- 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.
7384
- 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

Comments
 (0)