Skip to content

felilo/SUICoordinator

Repository files navigation

SUICoordinator

A comprehensive SwiftUI coordinator pattern library that provides powerful navigation management and tab-based coordination for iOS applications. SUICoordinator enables clean separation of concerns by decoupling navigation logic from view presentation, making your SwiftUI apps more maintainable and scalable.

Swift 6.0 iOS 16.0+ SwiftUI MIT License


Key Features

  • Pure SwiftUI: No UIKit dependencies - built entirely with SwiftUI
  • Coordinator Pattern: Clean separation of navigation logic from views
  • Tab Coordination: Advanced tab-based navigation with TabCoordinator, custom views, and badges
  • Flexible Presentations: Support for push, sheet, fullscreen, detents, and custom presentations
  • Deep Linking: Force presentation capabilities for push notifications and external triggers
  • Type-Safe Routes: Strongly typed navigation routes with compile-time safety
  • Async Navigation: Full async/await support for smooth navigation flows
  • Custom Tab Bars: Create completely custom tab interfaces with TabCoordinator
  • Badge Support: Dynamic badge management for tab items in TabCoordinator
  • Memory Management: Automatic cleanup and resource management

Quick Start

Installation

Swift Package Manager

  1. Open Xcode and your project
  2. Go to File → Swift Packages → Add Package Dependency...
  3. Enter the repository URL: https://github.com/felilo/SUICoordinator
  4. Click Next twice and then Finish

Manual Installation

  1. Download the source files from the Sources/SUICoordinator directory.
  2. Drag and drop the SUICoordinator folder into your Xcode project.
  3. Make sure to add it to your target.

Basic Usage

1. Define Your Routes

Create an enum that conforms to RouteType to define your navigation paths and their associated views.

import SwiftUI
import SUICoordinator

enum HomeRoute: RouteType {
    case push(dependencies: DependenciesPushView)
    case sheet(viewModel: DependenciesSheetView)
    
    var presentationStyle: TransitionPresentationStyle {
        switch self {
            case .push: .push
            case .sheet: .sheet
        }
    }

    @ViewBuilder
    var view: some View {
        switch self {
            case .push(let dependencies): PushView(viewModel: .init(dependencies: dependencies))
            case .sheet(let dependencies): SheetView(viewModel: .init(dependencies: dependencies))
        }
    }
}

For a deeper dive into route protocol, take a look at RouteType

2. Create Your Coordinator

Subclass Coordinator<YourRouteType> and implement the mandatory start() method. This method defines the initial view or flow for the coordinator.

import SUICoordinator

class HomeCoordinator: Coordinator<HomeRoute> {
    
    override func start() async {
        // This is the first view the coordinator will show.
        // 'startFlow' clears any existing navigation stack and presents this route.
        let viewModel = ActionListViewModel(coordinator: self)
        await startFlow(route: .actionListView(viewModel: viewModel), animated: animated)
    }
    
    // Example: Navigating to another view within this coordinator's flow
    func navigateToPushView() async {
        let viewModel = PushViewModel(coordinator: self)
        // Use the 'router' to navigate to other routes defined in HomeRoute.
        await router.navigate(toRoute: .push(viewModel: viewModel))
    }
    
    // Example: Presenting a sheet
    func presentSheet() async {
        let viewModel = SheetViewModel(coordinator: self)
        // Use 'presentSheet(route:)' for modal presentations.
        // The route's presentationStyle (e.g., .sheet, .fullScreenCover) will be used.
        await presentSheet(route: .sheet(viewModel: viewModel))
    }
    
    // Example: Navigating to another Coordinator (e.g., a TabCoordinator)
    func presentCustomTabs() async {
        let tabCoordinator = CustomAppTabCoordinator() // Assuming this is your TabCoordinator
        // The 'navigate(to:presentationStyle:)' method on a coordinator
        // is used to present another coordinator.
        await navigate(to: tabCoordinator, presentationStyle: .sheet)
    }
    
    func close() async {
        // 'router.close()' will either dismiss a presented sheet or pop a pushed view.
        await router.close(animated: true)
    }
    
    func endThisCoordinator() async {
        // 'finishFlow()' will dismiss/pop all views of this coordinator
        // and remove it from its parent coordinator.
        await finishFlow(animated: true)
    }
    
    func restart(animated: Bool = true) async {
        // Resets the coordinator's navigation state by calling `router.restart()`.
        // This clears all navigation stacks and modal presentations managed by this coordinator.
        // All navigation history will be lost, modal presentations dismissed, and the coordinator returns to its initial state as if `start()` was just called (depending on how `start()` and `startFlow()` are implemented).
        // Useful for logout scenarios or major state changes.
        await router.restart(animated: animated)
    }
}

3. Define Views and ViewModels

Your SwiftUI views will typically be initialized with a ViewModel. The ViewModel can hold a reference to its coordinator to trigger navigation actions.

// ActionListViewModel.swift
import Foundation

class ActionListViewModel: ObservableObject {
    let coordinator: HomeCoordinator // Or a protocol if you prefer
    
    init(coordinator: HomeCoordinator) {
        self.coordinator = coordinator
    }
    
    @MainActor func userTappedPush() async {
        await coordinator.navigateToPushView()
    }
    
    @MainActor func userTappedShowSheet() async {
        await coordinator.presentSheet()
    }

    @MainActor func userTappedShowTabs() async {
        await coordinator.presentCustomTabs()
    }
}

// NavigationActionListView.swift
import SwiftUI

struct NavigationActionListView: View {
    @StateObject var viewModel: ActionListViewModel
    
    var body: some View {
        List {
            Button("Push Example View") { Task { await viewModel.userTappedPush() } }
            Button("Present Sheet Example") { Task { await viewModel.userTappedShowSheet() } }
            Button("Present Tab Coordinator") { Task { await viewModel.userTappedShowTabs() } }
        }
        .navigationTitle("Coordinator Actions")
    }
}

4. Setup in your App

In your main App struct, instantiate your root coordinator and use its getView() method.

import SwiftUI
import SUICoordinator // Import the library

@main
struct SUICoordinatorExampleApp: App {
    
    // Instantiate your main/root coordinator
    var mainCoordinator = HomeCoordinator() // Or your primary app coordinator
    
    var body: some Scene {
        WindowGroup {
            mainCoordinator.getView()
        }
    }
}

How to implement a TabView?

The TabCoordinator<Page: TabPage> is a specialized coordinator for managing a collection of child coordinators, where each child represents a distinct tab in a tab-based interface.

1. Define Your Tab Pages (TabPage)

First, create an enum that conforms to TabPage. TabPage is a typealias for PageDataSource & TabNavigationRouter & SCEquatable.

  • PageDataSource: Requires you to define:
    • position: Int: The order of the tab (0-indexed).
    • dataSource: YourPageDataSourceType: An object/struct that provides the tab's visual elements (e.g., icon, title).
  • TabNavigationRouter: Requires you to implement:
    • coordinator() -> any CoordinatorType: A function that returns the specific Coordinator instance for this tab's navigation flow.
  • SCEquatable: Enums conform to Equatable automatically if all their raw values/associated values do. This is usually satisfied.
import SwiftUI
import SUICoordinator

// Step 1.1: Define the data source for your tab items' appearance
// This struct will hold the data for icons and titles for each tab.
public struct AppTabPageDataSource {
    let page: AppTabPage // A reference to the AppTabPage enum case

    @ViewBuilder public var icon: some View {
        switch page {
            case .home: Image(systemName: "house.fill")
            case .settings: Image(systemName: "gearshape.fill")
        }
    }

    @ViewBuilder public var title: some View {
        switch page {
            case .home: Text("Home")
            case .settings: Text("Settings")
        }
    }
}

// Step 1.2: Define your TabPage enum
enum AppTabPage: TabPage, CaseIterable { // CaseIterable is useful for providing .allCases
    case home
    case settings

    // PageDataSource conformance
    var position: Int {
        switch self {
            case .home: return 0
            case .settings: return 1
        }
    }

    var dataSource: AppTabPageDataSource {
        // Return an instance of your data source for this page
        AppTabPageDataSource(page: self)
    }

    // TabNavigationRouter conformance
    func coordinator() -> any CoordinatorType {
        // Return the specific coordinator instance for this tab's flow
        switch self {
            case .home: return HomeCoordinator() // Create and return a new instance
            case .settings: return SettingsCoordinator() // Create and return a new instance
        }
    }
}

Note: HomeCoordinator and SettingsCoordinator in the example above are regular Coordinator subclasses, each managing their own RouteType and views.

2. Create Your TabCoordinator Subclass

Subclass TabCoordinator<YourTabPageEnum>. In its initializer, you'll typically call super.init() providing:

  • pages: An array of your TabPage enum cases (e.g., AppTabPage.allCases.sorted(by: { $0.position < $1.position })).
  • currentPage: The TabPage case that should be selected initially.
  • presentationStyle (optional): How this TabCoordinator itself is presented by its parent (default is .sheet).
  • viewContainer: A closure that returns the SwiftUI view responsible for rendering the tab bar interface.
    • For SwiftUI's standard TabView, use TabViewCoordinator(dataSource: $0, currentPage: $0.currentPage).
    • For a completely custom tab bar UI, provide your own SwiftUI view that takes the TabCoordinator instance (as viewModel or dataSource). See CustomTabView.swift in the example project.
// Example: TabCoordinator using SwiftUI's default TabView
import SUICoordinator

// Example: TabCoordinator using a custom TabView UI
// (See CustomTabView.swift and CustomTabCoordinator.swift in the example project for a full implementation)
class CustomAppTabCoordinator: TabCoordinator<AppTabPage> {
    init(initialPage: AppTabPage = .home) {
        super.init(
            pages: AppTabPage.allCases,
            currentPage: initialPage,
            viewContainer: { dataSource in
                // Provide your custom tab bar view.
                CustomTabView(dataSource: dataSource)
            }
        )
    }
}

For a detailed example, you can take a look at the CustomTabView.swift implementation.

3. Using the TabCoordinator

Instantiate and start your TabCoordinator from a parent coordinator, just like any other coordinator.

// In a parent coordinator (e.g., your main AppRootCoordinator)
func showMainApplicationTabs() async {
    let tabCoordinator = CustomAppTabCoordinator()
    // Present the TabCoordinator. '.fullScreenCover' is common for main tab interfaces.
    await navigate(to: tabCoordinator, presentationStyle: .fullScreenCover)
}

Deep Linking or Push Notifications

SUICoordinator facilitates deep linking (e.g., from push notifications) by allowing you to programmatically navigate to specific parts of your application. The primary method for this is forcePresentation(mainCoordinator:) on a target coordinator. For tab-based applications, you'll combine this with TabCoordinator methods like setCurrentPage(with:) (or direct assignment to currentPage) and then use the child coordinator's router for further navigation.

General Strategy for Deep Linking:

  1. Identify the Target: Determine the ultimate destination:
    • If it's within a TabCoordinator, identify the TabCoordinator itself, the target TabPage, and the specific Route within that tab's child coordinator.
    • If it's a standalone flow, identify the Coordinator and its initial Route.
  2. Instantiate Coordinators: Create instances of the necessary coordinators. For a deep link into a tab, this usually means instantiating the relevant TabCoordinator.
  3. Force Present the Entry Coordinator: Call yourTargetCoordinator.forcePresentation(presentationStyle: mainCoordinator:).
    • yourTargetCoordinator is the coordinator that directly leads to the deep link's entry point (e.g., a TabCoordinator or a specific feature Coordinator).
    • mainCoordinator should be your application's root/main coordinator to establish the correct presentation context. This step ensures the target coordinator's view hierarchy becomes active, potentially dismissing or covering other views.
  4. Navigate to the Specific Tab (if applicable): If yourTargetCoordinator is a TabCoordinator:
    • Set its currentPage to the desired TabPage. For example: yourTabCoordinator.currentPage = .settingsTab.
    • A brief Task.sleep (e.g., try? await Task.sleep(nanoseconds: 100_000_000) for 0.1s) after setting currentPage can sometimes help ensure UI updates complete before further navigation.
  5. Navigate Within the Active Coordinator:
    • If it's a TabCoordinator, get the active child coordinator using try await yourTabCoordinator.getCoordinatorSelected().
    • Cast this child coordinator to its concrete type (e.g., SettingsCoordinator).
    • Use this coordinator's router to navigate to the final Route (e.g., await settingsCoordinator.router.navigate(toRoute: SettingsRoute.itemDetails(id: "itemID123"))).
@main
struct SUICoordinatorExampleApp: App {
    
    /// The main coordinator for the application, responsible for managing the primary tab-based navigation.
    /// It's an instance of `CustomTabCoordinator` which uses the standard SwiftUI `TabView`.
    var mainCoordinator = CustomTabCoordinator()
    
    /// The body of the app, defining the main scene.
    /// It sets up a `WindowGroup` containing the view provided by the `mainCoordinator`.
    /// - It includes `onReceive` for handling custom notifications that might trigger deep links.
    /// - It includes `onOpenURL` for handling URL-based deep links.
    /// - An `onAppear` modifier simulates an automatic deep link handling scenario after a 3-second delay
    ///   on application launch, demonstrating programmatic navigation.
    var body: some Scene {
        WindowGroup {
            mainCoordinator.getView()
                .onReceive(NotificationCenter.default.publisher(for: Notification.Name.PushNotification)) { object in
                    // Assumes `incomingURL` is accessible or passed via notification's object/userInfo
                    // For demonstration, let's assume `object.object` contains the URL string
                    guard let urlString = object.object as? String,
                          let path = DeepLinkPath(rawValue: urlString) else { return }
                    Task {
                        await try? handlePushNotificationDeepLink(path: path, rootCoordinator: mainCoordinator)
                    }
                }
                .onOpenURL { incomingURL in
                    guard let host = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true)?.host,
                          let path = DeepLinkPath(rawValue: host)
                    else { return }
                    
                    Task { @MainActor in
                        try? await handlePushNotificationDeepLink(path: path, rootCoordinator: mainCoordinator)
                    }
                }
        }
    }
    
    /// Defines possible deep link paths for the application.
    /// These raw string values would typically match URL schemes or notification payloads.
    /// - `home`: Represents a path to a home-like view, potentially within a tab, to present a detents sheet.
    /// - `tabCoordinator`: Represents a path to present a `CustomTabCoordinator` modally.
    enum DeepLinkPath: String {
        case home = "home" // Example: "yourapp://home" or a notification payload "home"
        case tabCoordinator = "tabs-coordinator" // Example: "yourapp://tabs/coordinator"
    }
    
    
    /// Handles deep link navigation based on the provided path.
    ///
    /// This function demonstrates how to programmatically navigate to specific parts of the app
    /// by interacting with the coordinator hierarchy. It's designed to be called from
    /// `onOpenURL`, `onReceive` (for notifications), or other app events.
    ///
    /// - Parameters:
    ///   - path: The `DeepLinkPath` indicating the destination within the app.
    ///   - rootCoordinator: The root `AnyCoordinatorType` instance of the application (e.g., `mainCoordinator`),
    ///     used as a starting point to traverse and manipulate the coordinator tree.
    /// - Throws: Can throw errors from coordinator operations, such as `topCoordinator()` or `getCoordinatorSelected()`,
    ///           if the navigation path is invalid or a coordinator is not in the expected state.
    func handlePushNotificationDeepLink(
        path: DeepLinkPath,
        rootCoordinator: AnyCoordinatorType
    ) async throws {
        switch path {
        case .tabCoordinator:
            // This case demonstrates deep linking to a view (with detents) within a specific tab.
            // It ensures that the action is performed on a HomeCoordinator if it's managing the currently selected tab.
            //
            // 1. `rootCoordinator.topCoordinator()`: Gets the topmost coordinator. This could be a modal's coordinator
            //    or the selected tab's coordinator if `rootCoordinator` is a TabCoordinator without a modal presented directly by it.
            // 2. `?.parent as? CustomTabCoordinator`: Checks if the parent of the topmost coordinator is our
            //    main `CustomTabCoordinator`. This confirms we are operating within the main tab structure.
            //    If true, `tabCoordinator` becomes this `CustomTabCoordinator` (which is `mainCoordinator`).
            // 3. `tabCoordinator.getCoordinatorSelected()`: Retrieves the coordinator for the *currently selected tab*
            //    from the (now confirmed) `CustomTabCoordinator`.
            // 4. `as? HomeCoordinator`: Checks if this selected tab's coordinator is an instance of `HomeCoordinator`.
            // 5. `await coordinatorSelected.presentDetents()`: If all checks pass, calls `presentDetents()` on the
            //    `HomeCoordinator` of the active tab, typically showing a sheet with detents.
            // This pattern allows for deep linking into a specific state (like showing a detents view) of a specific tab.
            if let tabCoordinator = try rootCoordinator.topCoordinator()?.parent as? CustomTabCoordinator {
                if let coordinatorSelected = try tabCoordinator.getCoordinatorSelected() as? HomeCoordinator {
                    await coordinatorSelected.presentDetents()
                }
            }
        case .home:
            // This case demonstrates presenting a different Coordinator modally (HomeCoordinator in this example).
            // It creates a new `HomeCoordinator` instance and uses `forcePresentation`
            // to display it as a sheet over the current context, managed by the `mainCoordinator`.
            let coordinator = HomeCoordinator()
            try? await coordinator.forcePresentation(
                presentationStyle: .sheet,
                mainCoordinator: mainCoordinator
            )
        }
    }
}

Example project

For a comprehensive understanding and more advanced use cases, including TabCoordinator examples (both default SwiftUI TabView and custom tab views), please explore the example project located in the Examples folder.

coordinator-ezgif com-resize


Features

These are the most important features and actions that you can perform:

RouteType

To create any route in SUICoordinator, your route definition (typically an enum) must conform to the RouteType protocol. This protocol is fundamental for defining navigable destinations within your application.

Protocol Requirements:

Conforming to RouteType (which also implies SCHashable) requires you to implement:

  1. presentationStyle: TransitionPresentationStyle:

    • A computed property that returns a TransitionPresentationStyle.
    • This determines how the view associated with the route will be presented. Possible values are:
      • .push: For navigation stack presentation (e.g., within a NavigationStack).
      • .sheet: Standard modal sheet presentation.
      • .fullScreenCover: A modal presentation that covers the entire screen.
      • .detents(Set<PresentationDetent>): A sheet presentation that can rest at specific heights (detents like .medium, .large, or custom heights). Requires iOS 16+.
        • Example: .detents([.medium, .large])
        • Example: .detents([.height(100), .fraction(0.75)])
      • .custom(transition: AnyTransition, animation: Animation?, fullScreen: Bool = false): Allows for custom SwiftUI transitions.
        • transition: The AnyTransition to use (e.g., .slide, .opacity, custom).
        • animation: An optional Animation to apply to the transition.
        • fullScreen: A Bool indicating if the custom transition should behave like a full-screen presentation (default false).
  2. view: Body:

    • A computed property, annotated with @ViewBuilder
    • It must return a type conforming to any View (SwiftUI's View protocol). Body is a typealias for any View within RouteType.
    • This property provides the actual SwiftUI view that will be displayed for this route.

Example Implementation:

import SwiftUI
import SUICoordinator

enum AppRoute: RouteType { // AppRoute now conforms to RouteType
    case login
    case dashboard(userId: String)
    case settings
    case itemDetails(itemId: String)
    case helpSheet
    case customTransitionView

    // 1. presentationStyle
    var presentationStyle: TransitionPresentationStyle {
        switch self {
            case .login:
                return .fullScreenCover // Login as a full screen cover
            case .dashboard, .itemDetails:
                return .push            // Dashboard and item details are pushed
            case .settings:
                return .sheet           // Settings are presented as a standard sheet
            case .helpSheet:
                return .detents([.medium, .large]) // Help sheet with detents
            case .customTransitionView:
                return .custom( // Example of a custom transition
                    transition: .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)),
                    animation: .easeInOut(duration: 0.5),
                    fullScreen: true
                )
        }
    }

    // 2. view
    @ViewBuilder
    var view: Body { // Body is 'any View'
        switch self {
            case .login:
                LoginView() // Assuming LoginView exists
            case .dashboard(let userId):
                DashboardView(userId: userId) // Assuming DashboardView exists
            case .settings:
                SettingsView() // Assuming SettingsView exists
            case .itemDetails(let itemId):
                ItemDetailView(itemId: itemId) // Assuming ItemDetailView exists
            case .helpSheet:
                HelpView() // Assuming HelpView exists
            case .customTransitionView:
                MyCustomAnimatedView() // Assuming MyCustomAnimatedView exists
        }
    }
}

By defining routes this way, SUICoordinator can manage the presentation and lifecycle of your views in a type-safe and structured manner. The SCHashable conformance allows routes to be used in navigation stacks and for SwiftUI to differentiate between them.

You can also use DefaultRoute for generic views if you don't need a specific enum for routes, as demonstrated in the TabFlowCoordinator example.

API

Router

The Router (a property on every Coordinator instance, e.g., coordinator.router) is responsible for managing the navigation stack and modal presentations for that specific coordinator. It abstracts navigation details, allowing views and ViewModels to request navigation changes without knowing the underlying SwiftUI mechanisms.


Name Parameters Description
navigate(toRoute:presentationStyle:animated:)
  • toRoute: Route (Your specific RouteType)
  • presentationStyle: TransitionPresentationStyle?, default: nil (uses route's default)
  • animated: Bool, default: true
Navigates to the given route. If the effective presentation style is .push, it pushes the view onto this router's navigation stack. Otherwise, it presents the view modally using this router's sheetCoordinator.
present(_:presentationStyle:animated:)
  • _ view: Route (Your specific RouteType)
  • presentationStyle: TransitionPresentationStyle?, default: .sheet (uses route's default if not .push, or .sheet if route's default is .push but a modal presentation is desired)
  • animated: Bool, default: true
Presents a view modally (e.g., sheet, fullScreenCover, detents) using this router's sheetCoordinator. If the presentation style is .push, it delegates to navigate(toRoute:).
pop(animated:)
  • animated: Bool, default: true
Pops the top view from this router's navigation stack.
popToRoot(animated:)
  • animated: Bool, default: true
Pops all views on this router's navigation stack except its root view.
dismiss(animated:)
  • animated: Bool, default: true
Dismisses the top-most modally presented view (sheet, fullScreenCover, etc.) managed by this router's sheetCoordinator.
close(animated:finishFlow:)
  • animated: Bool, default: true
Intelligently closes the top-most view: dismisses a sheet if one is presented by this router's sheetCoordinator, otherwise pops a view from this router's navigation stack.

Coordinator

The Coordinator is the brain for a specific navigation flow or feature. You subclass Coordinator<YourRouteType> to define navigation methods specific to that flow.


Name Parameters Description
router N/A (Property) Instance of Router specific to this coordinator. Use this for all navigation operations within this coordinator's flow (e.g., router.navigate(toRoute:), router.pop(), router.dismiss()).
start(animated:)
  • animated: Bool, default: true
**Must be overridden by subclasses.** This is where you define the initial view or flow for the coordinator, typically by calling await startFlow(route:animated:).
startFlow(route:transitionStyle:animated:)
  • route: Route (Your specific RouteType)
  • transitionStyle: TransitionPresentationStyle?, default: nil (uses route's default)
  • animated: Bool, default: true
Clears this coordinator's current navigation stack and any sheets it presented, then starts a new flow with the given route. This is essential for initializing or resetting the coordinator's view hierarchy.
finishFlow(animated:)
  • animated: Bool, default: true
Dismisses all views (pushed or presented) managed by *this* coordinator and removes *this* coordinator from its parent coordinator's children list. Effectively ends this coordinator's lifecycle and its associated UI.
forcePresentation(presentationStyle:animated:mainCoordinator:)
  • presentationStyle: TransitionPresentationStyle?, default: nil (uses presentation style of the coordinator's root view defined in its start() via startFlow())
  • animated: Bool, default: true
  • mainCoordinator: (any CoordinatorType)?, default: nil (attempts to find the top-most coordinator in the app to present from)
Forcefully presents *this* coordinator, even if other coordinators or views are active. Useful for handling deep links or push notifications. It will call this coordinator's start() method.
navigate(to:presentationStyle:animated:)
  • to: any CoordinatorType (Another coordinator instance)
  • presentationStyle: TransitionPresentationStyle?, default: nil (uses presentation style of the target coordinator's root view, or can be overridden)
  • animated: Bool, default: true
Navigates from *this* coordinator to *another* coordinator. It adds the target coordinator as a child, sets its parent, and calls the target coordinator's start() method. The presentation style determines how the new coordinator's view is shown (e.g., pushed onto this coordinator's stack, or presented as a sheet by this coordinator).
restart(animated:)
  • animated: Bool, default: true
Resets the coordinator's navigation state by calling router.restart(). This clears all navigation stacks and modal presentations managed by this coordinator. All navigation history will be lost, modal presentations dismissed, and the coordinator returns to its initial state as if start() was just called (depending on how start() and startFlow() are implemented). Useful for logout scenarios or major state changes.

TabCoordinator

Name Parameters / Type Description
router Router (Property) The router for the TabCoordinator itself (e.g., for how it's presented or if it needs to present something over the tabs). Not for navigation within individual tabs.
pages @Published var pages: [Page] (Property) The array of TabPage enums that define the tabs. Modifying this will update the tab bar.
currentPage @Published var currentPage: Page (Property) Get or set the currently selected TabPage. Changing this programmatically will switch the active tab.
setPages(_:currentPage:)
  • _ values: [Page]
  • currentPage: Page? (optional new current page if the old one is removed)
Asynchronously updates the set of pages (tabs) dynamically. Child coordinators for new pages are initialized, and old ones are cleaned up.
getCoordinatorSelected() Returns any CoordinatorType (Throws) Returns the child CoordinatorType instance that is currently active/selected based on currentPage. Throws TabCoordinatorError.coordinatorSelected if not found.
getCoordinator(with:)
  • position: Int
Returns AnyCoordinatorType?
Returns the child CoordinatorType instance at the given numerical position (index) in the tabs.
setBadge PassthroughSubject<(String?, Page), Never> (Property) A Combine subject to set or remove a badge on a tab. Send a tuple: ("badgeText", .yourTabPage) to set, or (nil, .yourTabPage) to remove the badge. The TabViewCoordinator and example CustomTabView handle displaying these.
viewContainer (TabCoordinator) -> Page.View (Property) A closure that you provide during initialization. It returns the SwiftUI view for the tab bar interface itself (e.g., `TabViewCoordinator` or your `CustomTabView`).

Key Steps:

  1. Instantiate Target Coordinator: Create an instance of the coordinator you want to present (e.g., MainTabCoordinator).
  2. forcePresentation: Call this on the instantiated coordinator, passing your app's current root/main coordinator. This makes the target coordinator active.
  3. Set State (if needed): For a TabCoordinator, update currentPage to the desired tab.
  4. Get Child Coordinator: Use getCoordinatorSelected() if navigating within a tab.
  5. Navigate in Child: Call relevant navigation methods on the child coordinator.

This approach ensures that the navigation hierarchy is correctly established before attempting to navigate to the final destination screen.


Contributing

Contributions to the SUICoordinator library are welcome! To contribute, simply fork this repository and make your changes in a new branch. When your changes are ready, submit a pull request to this repository for review.