How to use SwiftUI + Coordinators
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, we have been utilizing the MVVM (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.
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.
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.
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
.
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.
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.