I started building a SwiftUI project recently. The target I want to achieve is to build a project that can be used as a reference for building large production apps.
With that goal, I decided to apply Modular Architect and Clean Architect to the project.
While implementing the project, I faced some problems with the navigation. I also didn’t find any online solutions that could meet all my scenarios. In this article, I will write about navigation problems I met in the project and how I solved them.
Table of content
- Router module
- Open feature module’s view from the Application module
- Navigation inside feature module
- Navigation between 2 views in different modules
- Present a sheet
- Passing data between views
Before going to the main part, let’s take an overview look at the simple version of module dependencies in the project:
In this article, we will mainly focus on 4 modules: Application, Movies, Search, and Router.
1. Router module
Before iOS16, using NavigationView was not easy to customize (even if you want to popToRoot), that’s why we have a lot of different open sources for SwiftUI navigation.
With the release of NavigationStack, it’s easier to handle navigation logic, so I decided to use NavigationStack in the project.
To get started, created a Router class to wrap navigation logic and provide simpler APIs to interact with:
Since all feature modules need to use the Router, so I created a separate package for it. (You can see in the diagram that I’ve created a Router module and other modules depend on it.)
Using Router is dead simple:
2. Open Feature’s view from the Application module
Let’s say we want to open MovieView in the Movies module from the Application module. A simple approach is:
That will work. However, the drawback is that I will need to mark MovieViewModel, MovieUseCase, and MovieRepository as public so that they can be accessed outside the Movies module.
When developing a framework, I always try to hide the framework’s implementation details as much as possible. So the above approach can’t make me happy.
To solve that problem, I decided to go with the MVVM-C approach by creating Coordinators.
Another benefit of the Coordinator approach is that I can easily inject dependencies to the Movies module from the Application module:
3. Handle navigation inside a module
Enum-based should be the best approach when using NavigationStack. I defined an enum containing all destinations inside the module, and handle the navigation logic in the Coordinator.
Example of Movies module:
Note that the reason I named the enum PrivateDestination is that the enum can only be used for navigation between screens in the same module.
4. Navigation between 2 different modules
With Modular Architect, how can we navigate between 2 screens in different modules? For example, how to open SearchView (in the Search module) from MovieView (in the Movie module)?
The rule of thumb of Modular Architecture is that modules at the same level can’t depend on each other. That means the Movies module can’t open the Search module directly. Since the Application module knows about feature modules, I handled the navigation logic here:
Then we can handle the navigation logic between 2 feature modules in the Application module:
- Present a sheet
To present a sheet, I still used .sheet(item:content:) API. I expected that it should be easy and similar to how I handle navigation:
But I faced this issue with that approach:
To overcome this problem, I need to unbox existential types first. And my solution was to create AnyIdentifiable:
Now I could handle the sheet easily:
6. Passing data between views
How can we pass data from view2 back to view1? I thought it should be easy, just need to use closure or Binding as associated type:
And once again, not so fast 😣
With NavigationStack, the destination enum needs to conform Hashable protocol. Closure and Binding don’t conform to Hashable, so I need to made the enum conform to Hashable manually:
And that was good to go 🚀
Conclusion
Above are navigation problems I met and how I solved them in the project. If you know of any better solutions, don’t hesitate to leave a comment, I love to hear that.
You can find the source code here.
It would be awesome if you are interested in the project, contribution are always welcome.