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.
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:
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")
}
}
import SUICoordinator
class MainCoordinator: Coordinator<MainRoute> {
override init() {
super.init()
Task {
await startFlow(route: .splash, animated: false)
}
}
override func start(animated: Bool = true) async {
let coordinator = HomeCoordinator()
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()
}
}
}
- 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
}
}
- 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)
}
}
}
}
For better understanding, I recommend that you take a look at the example project located in the Example folder.
example_.mov
These are the most important features and actions that you can perform:
To create any route in SUICoordinator
you need to extend your object to the RouteType
protocol; Additionally, you can add your custom functions if you need. As you can see, in our example we are using custom types (enums
) to implement it.
Last but not least, you can also use DefaultRoute
to create custom routes as demonstrated in the TabBarFlowCoordinator
example
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(_) |
|
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(_) |
|
Is an async function, presents a view such as Sheet, Fullscreen or Detents |
pop(_) |
|
Is an async function, pops the top view from the navigation stack and updates the display. |
popToRoot(_) |
|
Is an async function, pops all the views on the stack except the root view and updates the display. |
dismiss(_) |
|
Is an async function, dismisses the view that was presented modally by the view. |
popToView(_) |
|
Is an async function, pops views until the specified view is at the top of the navigation stack. Example: router.popToView(MyView.self) |
close(_) |
|
Is an async function, dismiss or pops the last view presented in the 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(_) |
|
Is an async function, cleans the navigation stack and runs the navigation flow. |
finishFlow(_) |
|
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(_) |
|
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(_) |
|
Is an async function, allows you to navigate among the Coordinators. It calls the start() function. |
finishFlow(_) |
|
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. |
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() |
|
Returns the coordinator selected that is associated to the selected tab |
setPages(_) |
|
Is an async function, updates the page set. |
getCoordinator(_) |
|
Returns the coordinator at the position given as parameter |
setBadge |
|
Variable that allows set the badge of a tab |
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
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.