Every app that doesn't only consist of 1 single UI needs some form of navigation - to enable users to move between different screens and to brandish information or react to events.
Whether you apply navigation controllers, modal view controllers or some course of custom prototype - having a dainty way to perform navigation, in a way that doesn't paint you lot into corners, can be really tricky. Information technology'due south easy for view controllers to become tightly coupled and to have to spread dependencies out across your entire app.
This calendar week, let's take a look at a few different options for dealing with navigation in Swift apps, focusing on iOS this time.
Judo: Bring server-driven UI to your iOS and Android apps. Apace build native user interfaces visually, and publish them instantly, without having to submit updates to the App Store. Judo experiences fit seamlessly into your existing app, and is perfect for features similar onboarding and marketing offers. Build faster, release on your timeline, and save coding for the hard stuff.
The pushing problem
One of the standard ways to perform navigation on iOS is to use a UINavigationController that each view controller can either pop or button other view controllers onto, similar this:
class ImageListViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let prototype = images[indexPath.row] let detailVC = ImageDetailViewController(epitome: image) navigationController?.pushViewController(detailVC, blithe: true) } } While doing the above works totally fine (especially for simpler apps), information technology can become quite tricky to deal with every bit an app grows - in case yous want to be able to navigate to the same view controller from multiple places, or if you desire to implement something like deep linking from outside your app.
A case for Coordinators
One fashion to make navigation a bit more than flexible (and to avoid requiring view controllers to know about each other) is to utilise the coordinator pattern, as popularized by Soroush Khanlou's 2015 NSSpain talk. The thought is that you introduce an intermediate/parent object that coordinates multiple view controllers.
For example, let's say that nosotros're building an onboarding menstruation, where the user gets introduced to some of the key concepts of our app through a serial of screens. Instead of having each view controller push button the next one onto its navigationController, nosotros tin can use a coordinator that takes care of that.
We'll start past introducing a delegate protocol that our onboarding view controllers tin can use to notify their possessor when a button that should accept the user to the next screen was tapped:
protocol OnboardingViewControllerDelegate: AnyObject { func onboardingViewControllerNextButtonTapped( _ viewController: OnboardingViewController ) } class OnboardingViewController: UIViewController { weak var delegate: OnboardingViewControllerDelegate? private func handleNextButtonTap() { delegate?.onboardingViewControllerNextButtonTapped(self) } } We can then innovate a coordinator grade that acts equally the delegate for all onboarding view controllers and manages the navigation between them using a navigation controller:
class OnboardingCoordinator: OnboardingViewControllerDelegate { weak var delegate: OnboardingCoordinatorDelegate? private let navigationController: UINavigationController individual var nextPageIndex = 0 // Marker: - Initializer init(navigationController: UINavigationController) { self.navigationController = navigationController } // Mark: - API func activate() { goToNextPageOrFinish() } // MARK: - OnboardingViewControllerDelegate func onboardingViewControllerNextButtonTapped( _ viewController: OnboardingViewController) { goToNextPageOrFinish() } // Marking: - Private private func goToNextPageOrFinish() { // We employ an enum to store all content for a given onboarding page guard permit page = OnboardingPage(rawValue: nextPageIndex) else { consul?.onboardingCoordinatorDidFinish(self) return } let nextVC = OnboardingViewController(folio: folio) nextVC.delegate = self navigationController.pushViewController(nextVC, animated: true) nextPageIndex += i } } 1 big do good of using coordinators is that the navigation logic can be completely moved out of the view controllers themselves, giving you a lot more flexibility and enabling the view controllers to focus on what they do best - managing views 👍.
One thing to note above is that OnboardingCoordinator has a delegate of its own. We could either use this to have our AppDelegate manage the coordinator by retaining information technology and becoming its consul, or we could compose multiple levels of coordinators to construct most of our app'south navigation period. Nosotros could, for instance, accept an AppCoordinator that in turn manages an OnboardingCoordinator and whatsoever other coordinators that are on the aforementioned level in the navigation hierarchy. Pretty powerful stuff!
Where to, Navigator?
Another arroyo that tin be really useful (especially when edifice apps that take a large amount of screens and destinations that tin exist reached from multiple places) is to introduce dedicated navigator types.
To exercise that, we can beginning past creating a Navigator protocol that in turn has an associated blazon for what kind of Destination that information technology tin navigate to:
protocol Navigator { associatedtype Destination func navigate(to destination: Destination) } Using the to a higher place protocol, we can then implement multiple navigators that each perform the navigation within a given telescopic of our app - like for when before the user has logged in:
form LoginNavigator: Navigator { // Hither nosotros define a ready of supported destinations using an // enum, and nosotros tin can too utilize associated values to add together back up // for passing arguments from i screen to another. enum Destination { example loginCompleted(user: User) case forgotPassword case signup } // In nearly cases it's totally safe to make this a strong // reference, just in some situations it could end upwardly // causing a retain cycle, then better be safety than sorry :) private weak var navigationController: UINavigationController? // Marking: - Initializer init(navigationController: UINavigationController) { self.navigationController = navigationController } // Marking: - Navigator func navigate(to destination: Destination) { permit viewController = makeViewController(for: destination) navigationController?.pushViewController(viewController, blithe: truthful) } // MARK: - Private private func makeViewController(for destination: Destination) -> UIViewController { switch destination { case .loginCompleted(let user): return WelcomeViewController(user: user) instance .forgotPassword: return PasswordResetViewController() case .signup: return SignUpViewController() } } } Using navigators, navigating to a new view controller becomes as uncomplicated equally calling navigator.navigate(to: destination) and we don't need multiple levels of delegation to make it happen. All we demand is for each view controller to go along a reference to a navigator that supports all desired destinations, similar this:
class LoginViewController: UIViewController { individual allow navigator: LoginNavigator init(navigator: LoginNavigator) { self.navigator = navigator super.init(nibName: nil, parcel: cypher) } private func handleLoginButtonTap() { performLogin { [weak self] result in switch upshot { instance .success(let user): cocky?.navigator.navigate(to: .loginCompleted(user: user)) instance .failure(let error): self?.show(error) } } } private func handleForgotPasswordButtonTap() { navigator.navigate(to: .forgotPassword) } private func handleSignUpButtonTap() { navigator.navigate(to: .signup) } } We tin also take this a step further, and combine navigators with the factory pattern (like nosotros took a await at in "Dependency injection using factories in Swift"), to be able to easily move the creation of view controllers out from the navigators themselves, leaving united states of america with an even more than decoupled setup:
course LoginNavigator: Navigator { private weak var navigationController: UINavigationController? private let viewControllerFactory: LoginViewControllerFactory init(navigationController: UINavigationController, viewControllerFactory: LoginViewControllerFactory) { cocky.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: render viewControllerFactory.makePasswordResetViewController() case .signup: return viewControllerFactory.makeSignUpViewController() } } } When using something similar the above, nosotros also have an excellent opportunity to inject other types of navigators into our view controllers without making them aware of each other. For example, we might desire to inject a WelcomeNavigator into WelcomeViewController, which tin can be done in the mill instead of requiring LoginNavigator to exist aware of information technology 👍.
URLs and deep linking
For many kinds of apps we not just want to make it piece of cake to navigate within our own app, simply also to enable other apps & websites to deep link into ours. A mutual way to do this on iOS is to ascertain a URL scheme that other apps can then use to link directly into a specific screen or feature of our app.
Using either (or both!) coordinator and navigator objects, implementing URL and deep linking support becomes a lot easier, since we have defended places for navigation in which we can inject our URL treatment logic. We'll take a closer wait at URL-based navigation in an upcoming blog post.
Judo: Bring server-driven UI to your iOS and Android apps. Quickly build native user interfaces visually, and publish them instantly, without having to submit updates to the App Store. Judo experiences fit seamlessly into your existing app, and is perfect for features like onboarding and marketing offers. Build faster, release on your timeline, and save coding for the hard stuff.
Determination
Moving navigation logic out of view controllers and into dedicated objects like coordinators or navigators tin can make moving between multiple view controllers a lot simpler. What I like about both the coordinator & navigator approaches is that they are highly composable, making it possible to hands split out our navigation logic into multiple scopes & objects, rather than relying on some grade of primal routing.
Some other nice benefit is that both of these techniques let us remove non-optional optionals like when relying on a view controller's navigationController reference for our navigation logic. Normally, that leads to more anticipated and futurity-proof lawmaking.
What do you recall? Practise you lot employ some of these navigation techniques already, or do you lot have another favorite way of performing navigation? Allow me know, along with whatever questions, comments or feedback that yous might have on Twitter @johnsundell.
Cheers for reading! 🚀
DOWNLOAD HERE
Posted by: longoriasontoort.blogspot.com

0 Comments