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.
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 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
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
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
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, we use the
EnvironmentObject to pass it to other coordinators for handling navigation. Also, use the
AuthenticationCoordinator's output feature to switch from
MainView once the user logs 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
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.