How to use SwiftUI + Coordinators
Tags: ios, programming, swift
Published: 2024-01-12 08:52:18

---
title: How to use SwiftUI + Coordinators
published: true
date: 2024-01-09 12:25:37 UTC
tags: iOS,Programming,swift
canonical_url: https://www.rockandnull.com/how-to-use-swiftui-coordinators/
---

 ![How to use SwiftUI + Coordinators](https://images.unsplash.com/photo-1561821636-54d40db2a9ce?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDE2fHxmbG93fGVufDB8fHx8MTcwNDgwMzA0OXww&ixlib=rb-4.0.3&q=80&w=2000)

Transitioning from UIKit to SwiftUI can be both exciting and challenging. When I made a similar shift from Objective-C to Swift, I initially struggled by attempting to use Swift just like Objective-C, missing out on the unique advantages Swift had to offer. Learning from that experience, I was determined not to repeat the same mistake when moving from UIKit to SwiftUI.

At [Pale Blue](https://www.paleblueapps.com/), we have been utilizing the [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) (Model-View-ViewModel) + Coordinators software pattern with UIKit. Naturally, when I began working with SwiftUI, my initial impulse was to convert the existing UIKit logic directly to SwiftUI. However, it became apparent that this approach wasn’t feasible due to the fundamental differences between the two frameworks.

Realizing this, I paused to rethink and make sure that the Coordinators pattern, which worked well with UIKit, also fit well with SwiftUI. I began the process of adjusting and reshaping it to match the unique features and abilities of SwiftUI.

The big difference in the responsibilities of the Coordinators in SwiftUI is that we only have one NavigationStack (in contrast with UIKit) and that NavigationStack will handle every navigation using its [NavigationPath](https://developer.apple.com/documentation/swiftui/navigationpath) parameter.

In this post, we will create a simple app that will have a login view, a forgot password, and a TabBar with 3 different views to make it look like a real-life case. For simplicity, we will not use MVVM for now.

Let's start with `LoginView` and `ForgetPasswordView`. We will not add real functionality to them but we will mimic their behavior.

```
import SwiftUI

struct LoginView: View {
    // In MVVM the Output will be located in the ViewModel
    
    struct Output {
        var goToMainScreen: () -> Void
        var goToForgotPassword: () -> Void
    }

    var output: Output

    var body: some View {
        Button(
            action: {
                self.output.goToMainScreen()
            },
            label: {
                Text("Login")
            }
        ).padding()

        Button(
            action: {
                self.output.goToForgotPassword()
            },
            label: {
                Text("Forgot password")
            }
        )
    }
}

#Preview {
    LoginView(output: .init(goToMainScreen: {}, goToForgotPassword: {}))
}
```

_LoginView_

```
import SwiftUI

struct ForgotPasswordView: View {
    // In MVVM the Output will be located in the ViewModel
    struct Output {
        var goToForgotPasswordWebsite: () -> Void
    }

    var output: Output

    var body: some View {
        Button(
            action: {
                self.output.goToForgotPasswordWebsite()
            },
            label: {
                Text("Forgot password")
            }
        ).padding()
    }
}

#Preview {
    ForgotPasswordView(output: .init(goToForgotPasswordWebsite: {}))
}
```

_ForgotView_

There's nothing particularly unique about these two views, except for the Output struct. The purpose behind the Output struct is to relocate the navigation logic away from the view. Even if it doesn't seem clear at the moment, you'll grasp its functionality when we get into the `AuthenticationCoordinator` below.

Since both are views related to "authentication," we'll create an `AuthenticationCoordinator` that will handle their construction and navigation.

```
import Foundation
import SwiftUI

enum AuthenticationPage {
    case login, forgotPassword
}

final class AuthenticationCoordinator: Hashable {
    @Binding var navigationPath: NavigationPath

    private var id: UUID
    private var output: Output?
    private var page: AuthenticationPage

    struct Output {
        var goToMainScreen: () -> Void
    }

    init(
        page: AuthenticationPage,
        navigationPath: Binding<NavigationPath>,
        output: Output? = nil
    ) {
        id = UUID()
        self.page = page
        self.output = output
        self._navigationPath = navigationPath
    }

    @ViewBuilder
    func view() -> some View {
        switch self.page {
            case .login:
                loginView()
            case .forgotPassword:
                forgotPasswordView()
        }
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    static func == (
        lhs: AuthenticationCoordinator,
        rhs: AuthenticationCoordinator
    ) -> Bool {
        lhs.id == rhs.id
    }

    private func loginView() -> some View {
        let loginView = LoginView(
            output:
                .init(
                    goToMainScreen: {
                        self.output?.goToMainScreen()
                    },
                    goToForgotPassword: {
                        self.push(
                            AuthenticationCoordinator(
                                page: .forgotPassword,
                                navigationPath: self.$navigationPath
                            )
                        )
                    }
                )
        )
        return loginView
    }

    private func forgotPasswordView() -> some View {
        let forgotPasswordView = ForgotPasswordView(output:
                .init(
                    goToForgotPasswordWebsite: {
                        self.goToForgotPasswordWebsite()
                    }
                )
        )
        return forgotPasswordView
    }

    private func goToForgotPasswordWebsite() {
        if let url = URL(string: "https://www.google.com") {
            UIApplication.shared.open(url)
        }
    }

    func push<V>(_ value: V) where V : Hashable {
        navigationPath.append(value)
    }
}

```

_AuthenticationCoordinator_

A lot is happening within this context, so let's begin by examining the `AuthenticationPage` enum. Its role is to specify the page that the coordinator will initiate.

Moving on to the properties:

**navigationPath** : This property serves as a binding for `NavigationPath` and is injected from the `AppCoordinator`. Access to `NavigationPath` assists us in pushing our authentication views.

**id** : This represents a UUID assigned to each view, ensuring uniqueness during comparisons within the Hashable functions.

**output** : Similar to `LoginView` and `ForgotView`, Coordinators also possess an output. In the `AuthenticationCoordinator`, once the user is authenticated, transitioning to the main view becomes necessary. However, this transition is not the responsibility of the `AuthenticatorCoordinator`. Therefore, we utilize the output to inform the `AppCoordinator` that the authentication process is completed.

**authenticationPage** : This property's purpose is to define which page the coordinator will initialize.

Now let's examine the functions:

`func view() -> some View`: This function's role is to provide the appropriate view. It will be invoked within the `navigationDestination` of the `NavigationStack`, which is situated within our `SwiftUI_CApp`, which we'll explore later.

`private func loginView() -> some View`: This function returns the `LoginView` while also configuring its outputs.

`private func forgotPasswordView() -> some View`: Similar to the previous function, this returns the `ForgotPasswordView` while setting up its outputs.

`private func goToForgotPasswordWebsite()`: This function simulates opening a `URL` in Safari, resembling the action of accessing the forgot password webpage.

`func push(_ value: V) where V: Hashable`: This function appends a view to the `NavigationPath` provided in the initialization process.

Below is the AppCoordinator that we mentioned before. Its primary role is to serve as the main coordinator, responsible for initializing all other coordinators. To maintain simplicity, we'll encapsulate the SwiftUI components within a separate view called `MainView`.

```
import SwiftUI

final class AppCoordinator: ObservableObject {
    @Published var path: NavigationPath

    init(path: NavigationPath) {
        self.path = path
    }

    @ViewBuilder
    func view() -> some View {
        MainView()
    }
}
```

_AppCoordinator_

```
import SwiftUI

struct MainView: View {
    @EnvironmentObject var appCoordinator: AppCoordinator

    var body: some View {
        Group {
            AuthenticationCoordinator(
                page: .login,
                navigationPath: $appCoordinator.path,
                output: .init(
                    goToMainScreen: {
                        print("Go to main screen (MainTabView)")
                    }
                )
            ).view()
        }
    }
}

#Preview {
    MainView()
}

```

_MainView_

In `MainView`, we use the `AppCoordinator` through `EnvironmentObject` to pass it to other coordinators for handling navigation. Also, use the `AuthenticationCoordinator`'s output feature to switch from `LoginView` to `MainView` once the user logs in.

```
import SwiftUI

@main
struct SwiftUI_CApp: App {
    @StateObject private var appCoordinator = AppCoordinator(path: NavigationPath())

    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $appCoordinator.path) {
                appCoordinator.view()
                    .navigationDestination(
                        for: AuthenticationCoordinator.self
                    ) { coordinator in
                        coordinator.view()
                    }
            }
            .environmentObject(appCoordinator)
        }
    }
}

```

_SwiftUI_CApp_

In `SwiftUI_CApp` is where everything comes together. We begin by setting up `appCoordinator` using the `@StateObject` wrapper, to ensure its persistence during view updates. Next, a `NavigationStack` is created and supplied with the `NavigationPath` from `AppCoordinator`. This enables navigation as views are added or removed within the Coordinators. After constructing the `AppCoordinator` view, a `navigationDestination` is established for `AuthenticationCoordinator`. Lastly, we inject `appCoordinator` into the `NavigationStack`, making it available for all the views inside the stack.

And that's it for Part 1. In Part 2 we will wire up the Login/Logout logic and add a MainTabView simulating a real-case scenario,

You can find the source code [here](https://github.com/MMavrisPaleBlue/SwiftUI-Coordinators/tree/part1).