Implementing the brand new NavigationStack programmatically
Having simply revisited this navigation for SwiftUI 3 here, Apple has since rethought navigation with the brand new
NavigationStack as a part of the newest SwiftUI 4 launch. That is nice information… and covers most of my earlier options!
NavigationView required explicitly defining navigation “edges” and the usage of a number of flags which may result in confusion. The brand new method makes use of a stack making a non-UI illustration of the prevailing navigation and works fantastically with our earlier programmatic method with out many adjustments.
This method initially began with a evaluation of a multiscreen onboarding stream with SwiftUI. As with all multiscreen information entry flows, they usually signify an fascinating downside of decouple information, view, and navigation logic.
So, what makes an ideal multiscreen information entry stream? Right here’s what I got here up with. For need of a much less grand time period, I’ll name it my “display stream manifesto.” I exploit the “display” right here quite than view as a result of we’re explicitly referring to whole-screen navigation.
- Screens shouldn’t have any “mum or dad” information nor be accountable for navigating in or out.
- Particular person view fashions for each display.
- General stream management logic is separate from UI implementation and is testable with out UI.
- Versatile and permit for branching to totally different screens within the stream.
- So simple as potential however composable and scalable.
So onboarding could also be easy, maybe two or three screens asking the person some easy private info. A “subsequent” button would transfer the person ahead within the stream.
Nevertheless, what’s often extra typical is a extra complicated stream with branching. Perhaps the person isn’t able to share all these particulars but or maybe extra particulars are wanted relying on earlier responses. So, perhaps that is extra consultant:
As talked about beforehand, we might be utilizing
NavigationStack. This may be sure (2-way binding) to a navigation path. In our first implementation with only a 3-screen stream, we’re going to use
NavigationPath() which is a type-erased sequence. We’ll add the navigation path to our navigation mannequin and cross this round (extra later).
NavigationStack we outline a root view (in our case a
VStack with textual content and a button). This additionally comprises navigation vacation spot modifiers that set off the precise navigation. Any appending to the navigation path will level SwiftUI to the suitable new view for the brand new display based mostly on the kind and execute a push animation. This permits us to set off the precise navigation outdoors of the views (manifesto factors 1 and three) utilizing the
In our instance, including an integer to the trail will push a
ContentView2 to the stack, and including a string to the trail will push a
ContentView3. Now easy manipulation of solely the navigation path (which is a sequence) will instantly have an effect on navigation giving us full programmatic management. Right here lies the sweetness (and covers manifesto level 4)!
- Push: Append a specific sort.
- Again to root: Reinitialize the navigation path.
- Again 1 display: Take away the final worth.
We will additionally return a number of screens with
removeLast(x), though this isn’t proven under.
You’ll additionally word we now not want to make use of
NavigationLink in any respect.
NavigationLink remains to be an accessible choice in SwiftUI 4 however its most important use is in view-to-view navigation — one thing we need to keep away from right here!
On to manifesto level 2 — separate view fashions for every display. Utilization and implementation of view fashions could differ right here and I’ve heard issues concerning the overuse of the MVVM design sample in SwiftUI. It definitely isn’t a time period utilized in something official from Apple.
What I would like is a non-UI illustration of the view so I can cleanly encapsulate non-UI logic, unit take a look at it with out the view, and naturally, simply bind to the view (each methods). It additionally needs to be particular to the View so the View will be moved round and isn’t depending on something exterior (i.e., composable — manifesto level 5). It’s the interface of the view to the remainder of the applying. I name this a view mannequin.
ObservableObject (which is definitely a part of Mix) makes for a very good view mannequin that allows two-way view binding. The newer method with
@StateObject creates a secure view mannequin which is lazily loaded solely when wanted.
Notice additionally that on this model of a view mannequin, UI occasions are additionally handed into the view mannequin from the view, and any view-specific logic (e.g., community calls) could also be triggered from there (often calling right down to an API layer for instance).
To mannequin this out, now we have a stream view mannequin (
FlowVM) to handle the screen-to-screen navigation. It doesn’t know the views and is designed to be testable. It itself could require API calls to find out the trail to comply with. That is just like a “coordinator” however to me is taken into account a mannequin of the navigation and subsequently I’ve used the time period “view mannequin.”
Every display then additionally has particular person view fashions as nicely. These display view fashions deal with the UI occasions and display logic. In the end (upon completion of all display logic after a “subsequent” faucet for instance), we cross the management again from the display view fashions to the stream view mannequin to in the end resolve on the place to navigate.
For completion, eventing again from the display view fashions again “up” to the stream view mannequin, we are able to use a wide range of methods. Delegates and call-backs are all legitimate implementations however I like to make use of Mix’s
PassthroughSubject passing again a reference to the display view mannequin itself.
So the display view mannequin and the
view would love one thing like this:
And wired within the stream view mannequin to take heed to the completion occasions as follows utilizing a
sink and storing that in a
subscription. You’ll discover the manufacturing facility operate to create the display view mannequin is dealt with right here which additionally added the occasion listening. This manufacturing facility operate known as by the stream view in display view initialization.
sink calls a way on to deal with any logic (and navigation) and shops the subscription in a set connected to the view mannequin (which can be utilized for all subscriptions).
In our first implementation of the navigation stack, we used
NavigationPath and quite non-sensical sorts (integers and strings) to drive the navigation. As every display is now represented with a view mannequin we are able to really drive the navigation by including the view fashions themselves to the trail.
We may add the view fashions on to
NavigationPath and create a number of navigation vacation spot modifiers for every view mannequin sort. Nevertheless, this kind erased sequence provides solely restricted inquiry functionality (for instance — can’t simply inquire on what display is at the moment proven).
As a substitute, we are able to bind the navigation stack to a easy enum array that comprises an related worth of the view mannequin. Now the trail is an array now we have most management and introspection of its present state. The one requirement right here is that the array is
Hashable, which in flip requires the view fashions to be
Hashable. A bit of further work right here, however simple.
Try the repo for full code. This additionally contains examples of backward navigation (together with again to root or display two, and many others) and
An enormous a part of our design is to enhance testability and permit for unit assessments of the navigation stream unbiased of the UI (manifesto level 3). Now with view fashions, that is simply accomplished. Right here’s an instance:
We’re in a position to set off a “subsequent” button faucet after which verify the navigation logic has been triggered — all with out precise UI.
Notice although that is clearly a easy implementation. If the view fashions had API calls, we’d have to consider some injection to mock these out. As well as, that is clearly not a UI take a look at.
We may additionally need to add some UI assessments (maybe utilizing snapshot testing) — however that is past the scope of this text.
I hope this has made sense! After 4 variations, this iteration of SwiftUI’s push navigation is the method we’ve been searching for. This could now have answered many of the issues of the neighborhood at massive (in addition to my earlier options here).
Any enhancements? Proper now, there isn’t any apparent path to creating customized push navigation. An extra bigger thought is to create a single, converged navigation API for each push and modal navigation. Apple — your clock begins now!
The total code will be discovered at https://github.com/nickm01/NavigationFlow. Get pleasure from!