» » How to Implement Complex Navigation in iOS Apps for iPhone

How to Implement Complex Navigation in iOS Apps for iPhone

Every app, unless it's a single-page app, must have navigation that allows users to move between screens, enter information, and respond to events.
Whether you're using navigation controllers, modal view controllers, or something else, it can be tricky to get good navigation done without backing yourself into a corner.Most often, it turns out that view controllers are rigidly interconnected and pull dependencies throughout the application.

In this article, we will look at several ways to organize navigation in applications written in Swift.

How to Implement Complex Navigation in iOS Apps for iPhone

The problem of organization of navigation



One of the main ways to organize navigation in iOS is to use the UINavigationController , which allows you to add and remove other view controllers, like this:

class ImageListViewController : UITableViewController {
    override func tableView ( _ tableView : UITableView ,
                            didSelectRowAt indexPath : IndexPath ) {
        let image = images [ indexPath . row ]
        let detailVC = ImageDetailViewController ( image : image )
        navigationController? . pushViewController ( detailVC , animated: true } } _


Although the way shown above works well (especially for simple scenarios), it can become extremely difficult to develop applications written in this way. For example, when you need to navigate from different places to the same view controller or implement something like deep links to the application from the outside.

Arguments in favor of coordinators


One way to make navigation more flexible (while saving view controllers from having to know about each other) is to use the Coordinator design pattern. The idea is to add an intermediate/parent object that would coordinate multiple view controllers.
Let's say we're creating a series of screens for getting to know the application, where the user is briefly introduced to the main functionality. Instead of having each view controller add the next screen to the navigationController on its own, you can delegate this task to the coordinator.
Let's start by creating a delegate protocol that will allow our controllers to notify their owner when the next screen button is pressed:

protocol OnboardingViewControllerDelegate : AnyObject {
    func onboardingViewControllerNextButtonTapped (
        _ viewController : OnboardingViewController
    )
}

class OnboardingViewController : UIViewController {
    weak var delegate : OnboardingViewControllerDelegate?

    private func handleNextButtonTap ( ) {
        delegate? . onboardingViewControllerNextButtonTapped ( self )
    }
}


Then we add a coordinator class that will act as a delegate for all view controllers and manage navigation between them using a navigation controller:

class OnboardingCoordinator : OnboardingViewControllerDelegate {
    weak var delegate : OnboardingCoordinatorDelegate?

    private let navigationController : UINavigationController
    private var nextPageIndex = 0

    // MARK: - Initializer

    init ( navigationController : UINavigationController ) {
        self . navigationController = navigationController
    }

    // MARK: - API

    func activate ( ) {
        goToNextPageOrFinish ( )
    }

    // MARK: - OnboardingViewControllerDelegate

    func onboardingViewControllerNextButtonTapped (
        _ viewController : OnboardingViewController ) {
        goToNextPageOrFinish ( )
    }

    // MARK: - Private

    private func goToNextPageOrFinish ( ) {
        // We use an enum to store all content for a given onboarding page
        guard let page = OnboardingPage ( rawValue : nextPageIndex ) else {
            delegate? . onboardingCoordinatorDidFinish ( self )
            return
        }

        let nextVC = OnboardingViewController ( page : page )
        nextVC . delegate = self
        navigationController . pushViewController ( nextVC , animated : true )

        nextPageIndex += 1
    }
}


One of the main advantages of coordinators is the removal of navigation logic from view controllers. This allows view controllers to focus on what they do best - manage views.
Note that the OnboardingCoordinator has its own delegate. We can make the AppDelegate manage the coordinator by keeping it and thus becoming its delegate. Or use multiple levels of coordinators to organize the bulk of the navigation in the app. For example, an AppCoordinator can manage the OnboardingCoordinator and other coordinators at the same level of the navigation hierarchy. Pretty cool, isn't it?

Where are we going, navigator?


Another useful approach (especially for applications with many screens and a complex system of transitions between them) is to add specially designed types for this - navigators.
In order to do this, let's start by creating the Navigator protocol. Let's add an associated type to it, indicating which kind of Destination the transition is possible to:

protocol Navigator {
    associatedtype Destination

    func navigate ( to destination : Destination )
}


Using the above protocol, it is possible to implement several different navigators, each of which will navigate in a specific area of ​​the application. For example, before the user logs in to the application:

class LoginNavigator : Navigator {
    // Here we define a set of supported destinations using an
    // enum, and we can also use associated values ​​to add support
    // for passing arguments from one screen to another.
    enum Destination {
        case loginCompleted ( user : User )
        case forgotPassword
        case signup
    }

    // In most cases it's totally safe to make this a strong
    // reference, but in some situations it could end up
    // causing a retain cycle, so better be safe than sorry :)
    private weak varnavigationController : UINavigationController?

    // MARK: - Initializer

    init ( navigationController : UINavigationController ) {
        self . navigationController = navigationController
    }

    // MARK: - Navigator

    func navigate ( to destination : Destination ) {
        let viewController = makeViewController ( for : destination )
        navigationController? . pushViewController (viewController , animated : true )
    }

    // MARK: - Private

    private func makeViewController ( for destination : Destination ) -> UIViewController {
        switch destination {
        case . loginCompleted ( let user ) :
            return WelcomeViewController ( user : user )
        case . forgotPassword :
            return PasswordResetViewController ( )
        case . signup :
            return SignUpViewController ( )
        }
    }
}


With navigators, navigating to another view controller is a simple call to navigator.navigate(to: destination) , and we don't need multiple levels of delegation to do that. The only thing that each view controller needs is to store a reference to the navigator that supports all the necessary states:

class LoginViewController : UIViewController {
    private let navigator : LoginNavigator

    init ( navigator : LoginNavigator ) {
        self . navigator = navigator
        super . init ( nibName : nil , bundle : nil )
    }

    private func handleLoginButtonTap ( ) {
        performLogin { [ weak self ]result in
            switch result {
            case . success ( let user ) :
                self ? . navigator . navigate ( to : . loginCompleted ( user : user ) )
            case . failure ( let error ) :
                self ? . show ( error )
            }
        }
    }

    private func handleForgotPasswordButtonTap ( ) {
        navigator . navigate ( to : . forgotPassword )
    }

    private func handleSignUpButtonTap ( ) {
        navigator . navigate ( to : . signup )
    }
}


We can go even further and combine navigators using the Factory design pattern to move the creation of view controllers out of the navigators themselves and make the logic even more separated:

class LoginNavigator : Navigator {
    private weak var navigationController : UINavigationController?
    private let viewControllerFactory : LoginViewControllerFactory

    init ( navigationController : UINavigationController ,
         viewControllerFactory : LoginViewControllerFactory ) {
        self . navigationController = navigationController
        self . viewControllerFactory = viewControllerFactory
    }

    func navigate ( to destination : Destination ) {
        let viewController = makeViewController ( for : destination )
        navigationController? . pushViewController ( viewController , animated : true )
    }

    private func makeViewController ( for destination : Destination ) -> UIViewController {
        switch destination {
        case .loginCompleted ( let user ) :
            return viewControllerFactory . makeWelcomeViewController ( forUser : user )
        case . forgotPassword :
            return viewControllerFactory . makePasswordResetViewController ( )
        case . signup :
            return viewControllerFactory . makeSignUpViewController ( )
        }
    }
}


Using the approach described above, we have a wonderful opportunity to pass different types of navigators to view controllers in such a way that they do not know about each other. For example, using the "Factory" you can use the WelcomeNavigator inside the WelcomeViewController, while the LoginNavigator will not be aware of the existence of the WelcomeNavigator.

URLs and deep links


Often we want to not only simplify the navigation itself, but also allow other applications and websites to call our application through deep linking. The standard way to do this in iOS is to add a new URL scheme so that other apps can link to certain screens or features of our app.
Using coordinators/navigators (individually or together), it becomes much easier to implement support for URLs and deep links. After all, thanks to them, we have specific places where we can add link processing logic.

Conclusions


Moving navigation logic from view controllers to special objects like coordinators and navigators makes transitioning between multiple view controllers much easier. What we like about coordinators and navigators is that they make it easy to split the navigation logic into multiple scopes and objects, eliminating some form of centralized routing.
Another advantage is that there is no need to use non-optional optionals . For example, in navigation logic, you do not need to use a reference to the navigationController in the view controller. Which generally results in more predictable and maintainable code.

Related Articles

Add Your Comment

reload, if the code cannot be seen

All comments will be moderated before being published.