{"id":19440496,"url":"https://github.com/felilo/suicoordinator","last_synced_at":"2026-04-02T18:05:06.457Z","repository":{"id":219317181,"uuid":"748365070","full_name":"felilo/SUICoordinator","owner":"felilo","description":"Navigation coordinators for SWiftUI. Simple, powerful and elegant.","archived":false,"fork":false,"pushed_at":"2025-01-01T00:01:07.000Z","size":283,"stargazers_count":68,"open_issues_count":2,"forks_count":6,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-03-27T02:11:19.357Z","etag":null,"topics":["architecture-components","composition-root","coordinator-pattern","mvvm","mvvm-c","navigation-stack","swift","swiftui","viper"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felilo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-25T20:23:24.000Z","updated_at":"2025-03-19T23:55:03.000Z","dependencies_parsed_at":"2024-07-30T04:58:30.699Z","dependency_job_id":"78eb5214-f3be-4f80-b22d-4676c88ede2d","html_url":"https://github.com/felilo/SUICoordinator","commit_stats":null,"previous_names":["felilo/suicoordinator"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felilo%2FSUICoordinator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felilo%2FSUICoordinator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felilo%2FSUICoordinator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felilo%2FSUICoordinator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felilo","download_url":"https://codeload.github.com/felilo/SUICoordinator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248702999,"owners_count":21148116,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture-components","composition-root","coordinator-pattern","mvvm","mvvm-c","navigation-stack","swift","swiftui","viper"],"created_at":"2024-11-10T15:29:30.173Z","updated_at":"2026-04-02T18:05:06.449Z","avatar_url":"https://github.com/felilo.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SUICoordinator\n\nCoordinator-pattern navigation for SwiftUI. Keep navigation logic out of your views — type-safe routes, async/await, and full iOS 16+ support.\n\n[![Swift 6.0](https://img.shields.io/badge/Swift-6.0-orange.svg)](https://swift.org)\n[![iOS 16.0+](https://img.shields.io/badge/iOS-16.0+-blue.svg)](https://developer.apple.com/ios/)\n[![SwiftUI](https://img.shields.io/badge/Framework-SwiftUI-green.svg)](https://developer.apple.com/xcode/swiftui/)\n[![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n_____\n\n## Key Features\n\n- **Pure SwiftUI**: No UIKit — no `UINavigationController`, no view representables\n- **Type-Safe Routes**: Route enums own both the presentation style and the view they render\n- **Flexible Presentations**: Push, sheet, fullscreen, detents, and custom transitions\n- **Tab Coordination**: `TabCoordinator` with per-tab navigation stacks, custom tab views, and badges\n- **Deep Linking**: `forcePresentation` navigates to any coordinator from push notifications or universal links\n- **Dual iOS Support**: `SUICoordinator` (iOS 17+, `@Observable`) and `SUICoordinator16` (iOS 16+, `ObservableObject`)\n\n_____\n\n## Targets\n\nSUICoordinator ships two importable products that expose the same public API:\n\n- **`SUICoordinator`** (iOS 17+) — uses `@Observable`. Recommended for new projects.\n- **`SUICoordinator16`** (iOS 16+) — uses `ObservableObject` + Combine. See [SUICoordinator16.md](Docs/SUICoordinator16.md) for the full guide.\n\n_____\n\n## Installation\n\n### Swift Package Manager\n\n1. Open Xcode and your project\n2. Go to `File` → `Add Package Dependencies...`\n3. Enter the repository URL: `https://github.com/felilo/SUICoordinator`\n4. Select the package product that matches your deployment target (see [Targets](#targets) above)\n\n\u003e A single `import SUICoordinator` (or `import SUICoordinator16`) is all you need — all public types are available immediately.\n\n*Want to run something immediately?* **Clone the example app** → [Examples folder](https://github.com/felilo/SUICoordinator/blob/main/Examples/SUICoordinatorExample/SUICoordinatorExample).\n_____\n\n## Basic Usage\n\n### 1. Define Your Routes\n\nCreate an enum that conforms to `RouteType`. Each case maps to a SwiftUI view and declares how it should be presented.\n\n```swift\nimport SwiftUI\nimport SUICoordinator\n\nenum HomeRoute: RouteType {\n    case homeView(dependencies: DependenciesHomeView)\n    case pushView(dependencies: DependenciesPushView)\n    case sheetView\n\n    var presentationStyle: TransitionPresentationStyle {\n        switch self {\n            case .sheetView: .sheet\n            default: .push\n        }\n    }\n\n    @ViewBuilder\n    var body: some View {\n        switch self {\n            case .homeView(let dependencies): HomeView(dependencies: .init(dependencies))\n            case .pushView(let dependencies): PushView(dependencies: .init(dependencies))\n            case .sheetView: SheetView()\n        }\n    }\n}\n```\n\n### 2. Create Your Coordinator\n\n```swift\nimport SUICoordinator\n\n@Coordinator(HomeRoute.self)\nclass HomeCoordinator {\n\n    func start() async {\n        let dependencies = HomeViewDependencies()\n        await startFlow(route: .homeView(dependencies: dependencies))\n    }\n\n    func navigateToPushView() async {\n        let dependencies = PushViewDependencies()\n        await navigate(toRoute: .pushView(dependencies: dependencies))\n    }\n\n    func presentSheet() async {\n        await navigate(toRoute: .sheetView)\n    }\n    \n    func presentSheetWithCustomPresentationStyle() async {\n        await navigate(toRoute: .sheetView, presentationStyle: .detents([.medium, .large]))\n    }\n\n    func endThisCoordinator() async {\n        await finishFlow()\n    }\n}\n```\n\n\u003e **Without the macro**: If you prefer not to use the `@Coordinator` macro, you can subclass `Coordinator\u003cRoute\u003e` directly:\n\u003e ```swift\n\u003e class HomeCoordinator: Coordinator\u003cHomeRoute\u003e {\n\u003e\n\u003e     override func start() async {\n\u003e         let dependencies = HomeViewDependencies()\n\u003e         await startFlow(route: .homeView(dependencies: dependencies))\n\u003e     }\n\u003e }\n\u003e ```\n\n\u003e **iOS 16 support**: If your deployment target is iOS 16, use `SUICoordinator16` instead. See [SUICoordinator16.md](Docs/SUICoordinator16.md) for the complete guide.\n\n### 3. Define Views\n\nUse `@Environment(\\.coordinator)` to access the coordinator from your views. Cast it to the expected coordinator type:\n\n```swift\nimport SwiftUI\nimport SUICoordinator\n\nstruct HomeView: View {\n    @Environment(\\.coordinator) private var anyCoordinator\n\n    private var coordinator: HomeCoordinator? {\n        anyCoordinator as? HomeCoordinator\n    }\n\n    var body: some View {\n        List {\n            Button(\"Push Example View\") { Task { await coordinator?.navigateToPushView() } }\n            Button(\"Present Sheet Example\") { Task { await coordinator?.presentSheet() } }\n            Button(\"Present Tab Coordinator\") { Task { await coordinator?.presentDefaultTabs() } }\n        }\n        .navigationTitle(\"Coordinator Actions\")\n    }\n}\n```\n\n### 4. Setup in Your App\n\nInstantiate your root coordinator and use its `getView()` method.\n\n```swift\nimport SwiftUI\nimport SUICoordinator\n\n@main\nstruct MyExampleApp: App {\n\n    var rootCoordinator = HomeCoordinator()\n\n    var body: some Scene {\n        WindowGroup { rootCoordinator.getView() }\n    }\n}\n```\n\n_____\n\n## Example Project\n\nExplore working implementations of all features — push, sheet, fullscreen, detents, custom transitions, tab coordinators (default and custom), and deep linking.\n\n![coordinator-ezgif com-resize](https://github.com/user-attachments/assets/98a90863-3e35-48b3-9a9f-cf8757d5e0d6)\n\n[Examples folder →](https://github.com/felilo/SUICoordinator/tree/main/Examples/SUICoordinatorExample)\n\n_____\n\n## Tab Navigation\n\n`TabCoordinator\u003cPage: TabPage\u003e` manages a collection of child coordinators, one per tab.\n\n### 1. Define Your Tab Pages\n\nCreate an enum conforming to `TabPage` with three requirements:\n- `position: Int` — display order of the tab (0-indexed)\n- `dataSource` — a value providing the tab's visual elements (icon, title, etc.)\n- `coordinator() -\u003e any CoordinatorType` — the coordinator that manages this tab's flow\n\n```swift\nimport SwiftUI\nimport SUICoordinator\n\nstruct AppTabPageDataSource {\n    let page: AppTabPage\n\n    @ViewBuilder var icon: some View {\n        switch page {\n            case .homeCoordinator: Image(systemName: \"house.fill\")\n            case .settingsCoordinator: Image(systemName: \"gearshape.fill\")\n        }\n    }\n\n    @ViewBuilder var title: some View {\n        switch page {\n            case .homeCoordinator: Text(\"Home\")\n            case .settingsCoordinator: Text(\"Settings\")\n        }\n    }\n}\n\nenum AppTabPage: TabPage, CaseIterable {\n    case home\n    case settings\n\n    var position: Int {\n        switch self {\n            case .homeCoordinator: return 0\n            case .settingsCoordinator: return 1\n        }\n    }\n\n    var dataSource: AppTabPageDataSource {\n        AppTabPageDataSource(page: self)\n    }\n\n    func coordinator() -\u003e any CoordinatorType {\n        switch self {\n            case .home: return HomeCoordinator()\n            case .settings: return SettingsCoordinator()\n        }\n    }\n}\n```\n\n### 2. Create Your TabCoordinator\n\n```swift\nimport SUICoordinator\n\nclass DefaultTabCoordinator: TabCoordinator\u003cAppTabPage\u003e {\n    init(initialPage: AppTabPage = .home) {\n        super.init(\n            pages: AppTabPage.allCases,\n            currentPage: initialPage,\n            viewContainer: { dataSource in\n                DefaultTabView(dataSource: dataSource)\n            }\n        )\n    }\n}\n```\n\nFor a detailed example, see [DefaultTabView.swift](https://github.com/felilo/SUICoordinator/blob/main/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/TabCooridnators/DefaultTabCoordinator/DefaultTabView.swift). or [SplitView.swift](https://github.com/felilo/SUICoordinator/blob/main/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/SplitViewCoordinator/SplitView.swift).\n\n### 3. Present the TabCoordinator\n\n```swift\nfunc presentDefaultTabs() async {\n    let tabCoordinator = DefaultTabCoordinator()\n    await navigate(to: tabCoordinator, presentationStyle: .sheet)\n}\n```\n\n_____\n\n## Deep Linking\n\nNavigate to a specific part of the app from a push notification or a universal link using `forcePresentation(rootCoordinator:)`.\n\n**General strategy:**\n1. Identify the destination coordinator\n2. Call `forcePresentation(presentationStyle:rootCoordinator:)` on it\n3. For tab-based apps, set `currentPage` to the target tab, then navigate within the selected child coordinator\n\n```swift\n@main\nstruct MyExampleApp: App {\n\n    var rootCoordinator = DefaultTabCoordinator()\n\n    var body: some Scene {\n        WindowGroup {\n            rootCoordinator.getView()\n                .onReceive(NotificationCenter.default.publisher(for: Notification.Name.PushNotification)) { object in\n                    guard let urlString = object.object as? String,\n                          let path = DeepLinkPath(rawValue: urlString) else { return }\n                    Task { try? await handleDeepLink(path: path) }\n                }\n                .onOpenURL { incomingURL in\n                    guard let host = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true)?.host,\n                          let path = DeepLinkPath(rawValue: host) else { return }\n                    Task { @MainActor in try? await handleDeepLink(path: path) }\n                }\n        }\n    }\n\n    enum DeepLinkPath: String {\n        case home = \"home\"\n        case tabCoordinator = \"tabs-coordinator\"\n    }\n\n    @MainActor func handleDeepLink(path: DeepLinkPath) async throws {\n        switch path {\n        case .tabCoordinator:\n            if let coordinator = try rootCoordinator.getCoordinatorPresented() as? HomeCoordinator {\n                await coordinator.presentSheet()\n            } else {\n                let homeCoordinator = HomeCoordinator()\n                try await homeCoordinator.forcePresentation(rootCoordinator: rootCoordinator)\n                await homeCoordinator.presentSheet()\n            }\n        case .home:\n            let coordinator = HomeCoordinator()\n            try await coordinator.forcePresentation(\n                presentationStyle: .sheet,\n                rootCoordinator: rootCoordinator\n            )\n        }\n    }\n}\n```\n\n_____\n\n## Architecture Guides\n\nSUICoordinator works with any architecture. See the dedicated guides:\n\n- [MVVM](Docs/MVVM.md) — ViewModels delegate navigation to the coordinator\n- [TCA](Docs/TCA.md) — Navigation triggered from reducer effects via a dependency\n- [Decoupled Views](Docs/DecoupledViews.md) — Views with zero coordinator dependency\n\n_____\n\n## API Reference\n\nA `Coordinator` owns a `Router`, which drives both the navigation stack and modal presentations. Routes (`RouteType`) are the unit of navigation — each one declares how it should be presented (`presentationStyle`) and what it renders (`body`). You never interact with the `Router` directly in most cases; the `Coordinator` exposes convenience methods that delegate to it.\n\n```mermaid\nsequenceDiagram\n    participant C as Coordinator\n    participant R as Router\n    participant NS as NavigationStack\n    participant ML as Modal Layer\n\n    C-\u003e\u003eR: startFlow(route:)\n\n    Note over C,R: Push\n    C-\u003e\u003eR: navigate(toRoute: .push)\n    R-\u003e\u003eNS: push view\n\n    Note over C,ML: Modal\n    C-\u003e\u003eR: navigate(toRoute: .sheet / .fullScreenCover / .detents / .custom)\n    R-\u003e\u003eML: present view\n\n    Note over C,R: Dismiss / pop\n    C-\u003e\u003eR: close() / pop() / dismiss()\n    R--\u003e\u003eNS: pop view\n    R--\u003e\u003eML: dismiss view\n```\n\n_____\n\n### RouteType\n\nEvery route enum must conform to `RouteType`. Two requirements:\n\n- **`var presentationStyle: TransitionPresentationStyle`** — how the view is presented:\n\n| Style | Description |\n|-------|-------------|\n| `.push` | Pushes onto the navigation stack |\n| `.sheet` | Standard modal sheet |\n| `.fullScreenCover` | Modal covering the entire screen |\n| `.detents([...])` | Sheet that rests at specific heights (e.g., `.detents([.medium, .large])`) |\n| `.custom(transition:animation:fullScreen:)` | Custom SwiftUI transition |\n\n- **`var body: some View`** — the SwiftUI view rendered for that route case\n\n```swift\nimport SwiftUI\nimport SUICoordinator\n\nenum AppRoute: RouteType {\n    case login\n    case dashboard(userId: String)\n    case helpSheet\n    case customTransitionView\n\n    var presentationStyle: TransitionPresentationStyle {\n        switch self {\n            case .login: return .fullScreenCover\n            case .dashboard: return .push\n            case .helpSheet: return .detents([.medium, .large])\n            case .customTransitionView:\n                return .custom(\n                    transition: .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)),\n                    animation: .easeInOut(duration: 0.5),\n                    fullScreen: true\n                )\n        }\n    }\n\n    @ViewBuilder\n    var body: some View {\n        switch self {\n            case .login: LoginView()\n            case .dashboard(let userId): DashboardView(userId: userId)\n            case .helpSheet: HelpView()\n            case .customTransitionView: MyCustomAnimatedView()\n        }\n    }\n}\n```\n\n\u003e You can also use `DefaultRoute` for generic views when you don't need a typed route enum — as demonstrated in the [NavigationHubCoordinator example](https://github.com/felilo/SUICoordinator/blob/main/Examples/SUICoordinatorExample/SUICoordinatorExample/Coordinators/NavigationHubCoordinator/NavigationHubCoordinator.swift).\n\n_____\n\n### Coordinator\n\n| Method | Description |\n|--------|-------------|\n| `start()` | Override to define the initial view or flow, typically via `await startFlow(route:)`. |\n| `startFlow(route:)` | Clears the current stack and starts a new flow with the given route. |\n| `finishFlow(animated:)` | Dismisses all views of this coordinator and removes it from its parent. |\n| `navigate(toRoute:presentationStyle:animated:)` | Navigates to a route within this coordinator's flow. |\n| `navigate(to:presentationStyle:animated:)` | Presents another coordinator, adds it as a child, and calls its `start()`. |\n| `forcePresentation(presentationStyle:animated:rootCoordinator:)` | Presents this coordinator from the top of the hierarchy. Used for deep links. |\n| `restart(animated:)` | Resets the coordinator's navigation state to its initial route. |\n| `close(animated:)` | Dismisses if presented modally; pops if pushed. |\n| `getView()` | Returns the SwiftUI view for this coordinator. Use this to embed it in your app or another view. |\n| `getCoordinatorPresented(customRootCoordinator:)` | Returns the coordinator currently visible to the user, walking the full hierarchy. |\n\n_____\n\n### Router\n\nThe `Router` is available as `coordinator.router` and manages the navigation stack and modal presentations for a single coordinator's flow. Most navigation is done through the `Coordinator` methods above, but the `Router` is useful when you need lower-level control.\n\n| Method / Property | Description |\n|-------------------|-------------|\n| `navigate(toRoute:presentationStyle:animated:)` | Navigates to the given route. `.push` appends to the stack; all other styles present modally. |\n| `present(_:presentationStyle:animated:)` | Presents a view modally. Defaults to `.sheet` if no style is provided. |\n| `pop(animated:)` | Pops the top view from the navigation stack. |\n| `popToRoot(animated:)` | Pops all views except the root from the navigation stack. |\n| `dismiss(animated:)` | Dismisses the top-most modally presented view. |\n| `close(animated:)` | Dismisses if presented modally; pops if on the navigation stack. |\n| `restart(animated:)` | Clears all stacks and modal presentations, returning to the initial state. |\n\n_____\n\n### TabCoordinator\n\n`TabCoordinator` conforms to both `TabCoordinatorType` and `CoordinatorType`, so all methods from the [Coordinator](#coordinator) table are also available on a `TabCoordinator` instance.\n\nThe following properties and methods are specific to `TabCoordinator`:\n\n| Method / Property | Description |\n|-------------------|-------------|\n| `currentPage: Page` | Get or set the currently selected tab programmatically. |\n| `setCurrentPage(_:)` | Switches to the given tab, validating it exists and differs from the current one. |\n| `setPages(_:currentPage:)` | Dynamically updates the tab set, initializing coordinators for new pages and cleaning up removed ones. |\n| `getCoordinatorSelected()` | Returns the child coordinator for the currently selected tab (throws if not found). |\n| `getCoordinator(with:)` | Returns the child coordinator for a given `TabPage`, or `nil` if not found. |\n| `setBadge(for:with:)` | Sets or removes a badge on a tab. Pass `nil` as the value to remove it. |\n| `popToRoot()` | Pops the active tab's navigation stack to its root view. |\n\n\n_____\n\n## Contributing\n\nContributions are welcome! Fork the repository, make your changes in a new branch, and open a pull request for review.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelilo%2Fsuicoordinator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelilo%2Fsuicoordinator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelilo%2Fsuicoordinator/lists"}