iOS app navigation synchronization with NavigationQueue
Hello, today I wanted to talk with you about mobile app navigation. In one of the projects I worked with, we had a problem. We needed to present multiple screens in a sequence. The application had quick actions, push notifications, deep links, and several popups that may show up on the app start. To solve this challenge, our team came up with an idea to use a queue to perform navigation operations. This approach allowed us to push navigation operations to a separate component that performs navigation in a sequence. This component turned out to be a great tool. Now we are using it in multiple projects. Therefore, I wanted to share our solution with you.
Should we use callbacks and flags?
First, let’s look at a common way to solve this problem and its drawbacks. If you came here just for the goodies, you may go ahead and skip this section.
When we encounter this kind of problem, the first thought that comes to our mind is probably callbacks and flags. Let us explore an example of a similar implementation. Here we have code that handles an app shortcut and presents a marketing popup simultaneously on app start.
It doesn’t look pretty, right? We need to couple two different functionalities to ensure that navigation works correctly. Now, imagine that you may have an error alert popping up from any view controller that was currently on top, and it becomes a nightmare to handle. There has to be a better way.
NavigationQueue to the rescue
Solution overview
As I said before, we came up with an idea to use a queue to do all the heavy lifting of navigation. The architecture of our solution looks like this:
- NavigationQueue — the main class is responsible for queueing and executing navigation operations in a sequence.
- NavigationOperation — the base class that provides a template and common logic for all navigation operations.
- ShowNavigationOperation — a concrete operation class that uses the `show` method to present a view controller.
- PresentModalOperation — a navigation operation class that presents a view controller modally.
Next, let’s dig deeper and see how we have implemented each of these four components.
NavigationQueue
NavigationQueue
is the main class that handles the queueing and execution of navigation actions. As you can see below, this class is pretty straightforward. iOS already has a great Foundation API for operations and queues — called OperationQueue
. Therefore, we decided to implement NavigationQueue
as a wrapper for this class.
Here’s what we have done:
- We created a standard operation queue that does all the hard work. Configured it to perform one operation at a time because we wanted to run them in a sequence. Additionally, we specified quality of service to
user-interactive
because operations execute actions associated with UI. - Next, we created a singleton instance of the
NavigationQueue
. It was needed to ensure that the application used a single queue. Otherwise, we would not have the desired navigation synchronization. - Finally, we wanted to limit what operations can be run by this queue. Therefore, we have not exposed the internal operation queue and wrote a method that allows only the subclasses of
NavigationOperation
added.
NavigationOperation
Next, we have the NavigationOperation
class. There were two design choices for this API component: define it as a protocol or implement it as an abstract class. I usually prefer protocols. Despite that, we chose the abstract class. It fits here better because there are numerous things that we need to override from Foundation.Operation
, and the abstract class allows us to do it just once. And yes, I do understand that Swift doesn’t yet have a native abstract class feature, but there is a way to make a pseudo abstract class in Swift. Let’s see how we have implemented it.
- First of all, we needed to override and implement some states to make the
Operation
subclass work. If you have ever used aFoundation.OperationQueue
, you should be familiar with them. If not, there are some great tutorials on the internet. - Secondly, we overrode the
start
method from theOperation
class. This method performs the actual work of the operation. We check and update the internal operation state and call theexecute
to run the logic. - Thirdly, as you may know, operations are executed on background threads, and navigation performs on the main UI thread. Therefore, we made sure to call
execute
from the main GCD queue. This way, we don’t need to worry about multi-threading inside concrete navigation operation implementations. - Next, we wrote an abstract method that performs the actual navigation logic. We made sure that the subclasses override this method by adding a
fatalError
. - Finally, we added a method that updates the internal operation state when an operation finishes. Subclasses must call it after the navigation action.
ShowNavigationOperation
To use operation queue in action, we need concrete navigation operations. Let’s start by looking at ShowNavigationOperation
.This operation wraps the UIViewController.show(_:sender:)
navigation method.
- We defined source and destination view controllers involved in navigation action. And an optional sender passed to the
show
method. - A property specifying the operation duration.
- Navigation logic is implemented inside the
execute
method. It performs theshow
call. - As you know, the view controller’s
show
method doesn’t have any completion handler. Therefore, we used a delayed call from GCD API to finish this operation after 0.4 s.
PresentModalOperation
Next, we have the PresentModalOperation
, an operation for cases where we specifically want to present a view controller modally. Let’s take a look at the code how we implemented it.
- First, we have a modal view controller that will be presented and a presenting view controller that presents it.
- Second, we have a state property that specifies if the presentation should be animated.
- Third, execute method calls view controller
present
method and invokefinish
in completion closure. This operation is very straightforward.
Bonus! Let’s write some extensions
As a bonus, I will give you a couple of extensions that will make it very easy to use the navigation queue inside your code. These extensions act as wrappers and allow you to isolate navigation operations creation.
Putting it all together
Let’s put it all together and see what the example provided at the beginning of this post looks like with NavigationQueue
.
As you can see, NavigationQueue
has allowed us to simplify this logic. We no longer need any flags to synchronize navigation operations, and we have separated the quick actions and marketing popup handling. Another great thing is those extensions I have previously shown. They allow us to use NavigationQueue
without any overhead. We only need to replace show
and present
methods with their counterparts enqueueShow
and enqueuePresent
.
Final thoughts
In this article, I have presented you with our team’s approach to navigation synchronization in iOS applications. We have decided to implement a NavigationQueue
component that manages navigation operations for us. This approach allows us to separate different app features and is quite agile: it is simple to write various navigation operations. If you find it useful, feel free to use this idea in your projects. The examples I have provided are somewhat simplified and only used as proof of concept. Therefore, they don’t consider the edge cases and are not ready for production code, use them of your own accord.