SUICoordinator

0.0.4

Navigation coordinators for SWiftUI. Simple, powerful and elegant.
felilo/SUICoordinator

What's New

V0.0.4

2024-01-29T18:06:24Z

Features

  • Fixes mainView loading

SUICoordinator

This repository contains a library that implements the Coordinator pattern, which is a design pattern used in iOS application development to manage application navigation flows. The library provides a set of features that can be used to implement the Coordinators flow. This library does not use any UIKit components.


Getting Started

To use the SUICoordinator library in your iOS project, you'll need to add the library files to your project. Here are the basic steps:


Defining the coordinator

First let's define the paths and views.


import SwiftUI
import SUICoordinator

enum HomeRoute: RouteType {
    
    case push(viewModel: PushViewModel)
    case sheet(viewModel: SheetViewModel)
    case fullscreen(viewModel: FullscreenViewModel)
    case detents(viewModel: DetentsViewModel)
    case actionListView(viewModel: ActionListViewModel)
    
    var presentationStyle: TransitionPresentationStyle {
        switch self {
            case .push:
                return .push
            case .sheet:
                return .sheet
            case .fullscreen:
                return .fullScreenCover
            case .detents:
                return .detents([.medium])
            case .actionListView:
                return .push
        }
    }
    
    var view: Body {
        switch self {
            case .push(let viewModel):
                return PushView(viewModel: viewModel)
            case .sheet(let viewModel):
                return SheetView(viewModel: viewModel)
            case .fullscreen(let viewModel):
                return FullscreenView(viewModel: viewModel)
            case .detents(let viewModel):
                return DetentsView(viewModel: viewModel)
            case .actionListView(let viewModel):
                return NavigationActionListView(viewModel: viewModel)
        }
    }
}

Second let's create the first Coordinator. All coordinator should to implement the start() function and then starts the flow (mandatory). Finally, add additional flows

import SUICoordinator

class HomeCoordinator: Coordinator<HomeRoute> {
    
    override func start(animated: Bool = true) async {
        let viewModel = ActionListViewModel(coordinator: self)
        await startFlow(route: .actionListView(viewModel: viewModel), animated: animated)
    }
    
    func navigateToPushView() async {
        let viewModel = PushViewModel(coordinator: self)
        await router.navigate(to: .push(viewModel: viewModel))
    }
    
    func presentSheet() async {
        let viewModel = SheetViewModel(coordinator: self)
        await router.navigate(to: .sheet(viewModel: viewModel))
    }
    
    func presentFullscreen() async {
        let viewModel = FullscreenViewModel(coordinator: self)
        await router.navigate(to: .fullscreen(viewModel: viewModel))
    }
    
    func presentDetents() async {
        let viewModel = DetentsViewModel(coordinator: self)
        await router.navigate(to: .detents(viewModel: viewModel))
    }
    
    func presentTabbarCoordinator() async {
        let coordinator = TabbarFlowCoordinator()
        await navigate(to: coordinator, presentationStyle: .sheet)
    }
    
    func close() async {
        await router.close()
    }
    
    func finsh() async {
        await finishFlow(animated: true)
    }
}

Then let's create a View and its ViewModel

import Foundation

class ActionListViewModel: ObservableObject {
    
    let coordinator: HomeCoordinator
    
    init(coordinator: HomeCoordinator) {
        self.coordinator = coordinator
    }
    
    func navigateToPushView() async {
        await coordinator.navigateToPushView()
    }
    
    func presentSheet() async {
        await coordinator.presentSheet()
    }
    
    func presentFullscreen() async {
        await coordinator.presentFullscreen()
    }
    
    func presentDetents() async {
        await coordinator.presentDetents()
    }
    
    func presentTabbarCoordinator() async {
        await coordinator.presentTabbarCoordinator()
    }
    
    func finsh() async {
        await coordinator.finsh()
    }
}
import SwiftUI

struct NavigationActionListView: View {
    
    typealias ViewModel = ActionListViewModel
    @StateObject var viewModel: ViewModel
    
    var body: some View {
        List {
            Button("Push NavigationView") {
                Task { await viewModel.navigateToPushView() }
            }
            
            Button("Presents SheetView") {
                Task { await viewModel.presentSheet() }
            }
            
            Button("Presents FullscreenView") {
                Task { await viewModel.presentFullscreen() }
            }
            
            Button("Presents DetentsView") {
                Task { await viewModel.presentDetents() }
            }
            
            Button("Presents Tabbar Coordinator") {
                Task { await viewModel.presentTabbarCoordinator() }
            }
        }
        .navigationTitle("Navigation Action List")
    }
}

3. Create MainCoordinator and its Routes

import SUICoordinator

class MainCoordinator: Coordinator<MainRoute> {
    
    override init() {
        super.init()
    }
    
    override func start(animated: Bool = false) async {
        let coordinator = HomeCoordinator()
        async let _ = await startFlow(route: .splash, animated: false)
        await navigate(to: coordinator, presentationStyle: .fullScreenCover, animated: animated)
    }
}
import SUICoordinator
import SwiftUI

enum MainRoute: RouteType {
    
    case splash
    
    public var presentationStyle: TransitionPresentationStyle {
        switch self {
            case .splash:
                return .push
        }
    }
    
    @ViewBuilder
    public var view: any View {
        switch self {
            case .splash:
                SplashView()
        }
    }
}

Setup project


  1. Create an AppDelegate class and do the following implementation or if you prefer skip this step and do the same implementation in the next step.
import SwiftUI
import SUICoordinator

class AppDelegate: NSObject, UIApplicationDelegate {
    
    var mainCoodinator: (any CoordinatorType)?
    
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
    ) -> Bool {
        mainCoodinator = MainCoordinator()
        return true
    }
}

  1. In the App file, Follow next implementation:
import SwiftUI

@main
struct SUICoordinatorDemoApp: App {
    
    @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
    
    var body: some Scene {
        WindowGroup {
            if let view = appDelegate.mainCoodinator?.view {
                AnyView(view)
            }
        }
    }
}

Example project

For better understanding, I recommend that you take a look at the example project located in the Example folder.

example_.mov

Features

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

Router

The router is encharge to manage the navigation stack and coordinate the transitions between different views. It abstracts away the navigation details from the views, allowing them to focus on their specific features such as:


Name Parametes Description
navigate(_)
  • to: Route,
  • presentationStyle: TransitionPresentationStyle?, default: nil,
  • animated: Bool?, default true,
Is an async function, allows you to navigate among the views that were defined in the Route. The types of presentation are Push, Sheet, Fullscreen and Detents
present(_)
  • _ view: ViewType
  • presentationStyle: TransitionPresentationStyle?, default: nil,
  • animated: Bool?, default true,
Is an async function, presents a view such as Sheet, Fullscreen or Detents
pop(_)
  • animated: Bool?, default true,
Is an async function, pops the top view from the navigation stack and updates the display.
popToRoot(_)
  • animated: Bool?, default true,
Is an async function, pops all the views on the stack except the root view and updates the display.
dismiss(_)
  • animated: Bool?, default true,
Is an async function, dismisses the view that was presented modally by the view.
popToView(_)
  • _ view: T
  • animated: Bool?, default true,
Is an async function, pops views until the specified view is at the top of the navigation stack. Example: router.popToView(MyView.self)
close(_)
  • animated: Bool?, default true,
Is an async function, dismiss or pops the last view presented in the Coordinator.

Coordinator

Acts as a separate entity from the views, decoupling the navigation logic from the presentation logic. This separation of concerns allows the views to focus solely on their specific functionalities, while the Navigation Coordinator takes charge of the app's overall navigation flow. Some features are:


Name Parametes Description
router Variable of Route type which allow performs action router.
startFlow(_)
  • to: Route
  • transitionStyle: TransitionPresentationStyle?, default: automatic,
  • animated: Bool?, default true
Is an async function, cleans the navigation stack and runs the navigation flow.
finishFlow(_)
  • animated: Bool?, default true,
Is an async function, pops all the views on the stack including the root view, dismisses all the modal view and remove the current coordinator from the coordinator stack.
forcePresentation(_)
  • route: Route
  • presentationStyle: TransitionPresentationStyle?, default: automatic,
  • animated: Bool?, default true,
  • mainCoordinator: Coordinator?, default: mainCoordinator
Is an async function, puts the current coordinator at the top of the coordinator stack, making it the active and visible coordinator. This feature is very useful to start the navigation flow from push notifications, notification center, atypical flows, etc.
navigate(_)
  • to: Coordinator
  • presentationStyle: TransitionPresentationStyle?, default: automatic,
  • animated: Bool?, default true,
Is an async function, allows you to navigate among the Coordinators. It calls the start() function.
finishFlow(_)
  • animated: Bool?, default true,
Is an async function, pops all the views on the stack including the root view, dismisses all the modal view and remove the current coordinator from the coordinator stack.

TabbarCoordinator

It works the same as Coordinator but has the following additional features:


Name Parametes Description
currentPage Variable of Page type which allow set and get the tab selected
getCoordinatorSelected()
  • mainCoordinator: Coordinator?, default mainCoordinator,
Returns the coordinator selected that is associated to the selected tab
setPages(_)
  • _values: [PAGE]?, default mainCoordinator,
Is an async function, updates the page set.
getCoordinator(_)
  • position: Int
Returns the coordinator at the position given as parameter
setBadge
  • PassthroughSubject: (String?, Page)?
Variable that allows set the badge of a tab

Installation 💾

SPM

Open Xcode and your project, click File / Swift Packages / Add package dependency... . In the textfield "Enter package repository URL", write https://github.com/felilo/SUICoordinator and press Next twice


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.

License

The SUICoordinator library is released under the MIT license. See the LICENSE file for more information.

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sat Jun 15 2024 09:28:00 GMT-0900 (Hawaii-Aleutian Daylight Time)