SnapNavigation 1.0.2

SnapNavigation 1.0.2

Maintained by Stephen Downs.



SnapNavigation

Composable view navigation for iOS

Language: Swift Platform: iOS 8+ License: MIT

SnapNavigation provides a comprehensive way to define and handle all iOS application view navigation concerns.

SnapNavigation allows you to separate navigation code into dedicated objects, empowering you to snap together each aspect in any way you see fit: dynamically determining source and destination views, mediation between actors, and presentational code. It works with navigation actions triggered from code as well as from UIStoryboard action segues.

Quick Start

If you want to skip the details and use SnapNavigation right away, the following example recipes offer some starting points.

Basic composable navigation examples
import UIKit

// Basic SnapNavigator with no internal navigation logic, just default implementations.
class MyNavigator: SnapNavigator {}

// Some particular view controller in your application.
class MyViewController: UIViewController {

    // Create a SnapNavigator that uses only the default implementation methods.
    let navigator = MyNavigator()

    var someData: String = "Sample data"

    // Example navigations.
    func composableNavigationExamples() {

        // Note: Some examples show internal creation of a destination view controller. This is not good practice unless the destination is intended as an internally controlled child view controller.

        // Composable navigation example 1:
        //  Create a secondary view controller, and show it.
        let destinationVC = UIViewController()
        navigator.navigate(from: .viewController(self), to: .viewController(destinationVC), with: .show)

        // Composable navigation example 2:
        //  Create a secondary view controller, pass a value to it via closure and show it.
        let destinationWithValueVC = MyViewController()
        let mediation2: (UIViewController, UIViewController) -> () = { source, destination in
            if let destination = destination as? MyViewController {
                destination.someData = "Sent message"
            }
        }
        navigator.navigate(from: .viewController(self), to: .viewController(destinationWithValueVC), applying: .method(mediation2), with: .show)

        // Composable navigation example 3:
        //  Navigation using a set navigation model, presenting a second VC and passing a value.
        let destination3 = MyViewController()
        let mediation3: (UIViewController, UIViewController) -> () = { source, destination in
            if let destination = destination as? MyViewController {
                destination.someData = "Sent message 3"
            }
        }
        let navigation3 = SnapNavigation(
            source: .viewController(self),
            destination: .viewController(destination3),
            mediation: .method(mediation3),
            presentation: SnapNavigation.Presentation.present(true, {}))
        navigator.navigate(using: navigation3)

    }

}
Navigation provider example
// Navigation using provider:
//  Explicitly set the navigator data provider.
//  We set ourself as the data provider in this example. See the SnapNavigatorDataSource extension below.
navigator.navigationProvider = self
navigator.navigate()

extension MyViewController: SnapNavigatorDataSource {
    func navigation(for navigator: SnapNavigator) -> SnapNavigation {
        let someDestination = UIViewController()
        let someMediation: (UIViewController, UIViewController) -> () = { source, destination in
            destination.title = "Honey I Set the Title!"
        }
        return SnapNavigation(
            source: .viewController(self),
            destination: .viewController(someDestination),
            mediation: .method(someMediation),
            presentation: .show)
    }
}
Navigation with a UIStoryboardSegue
//  An example SnapNavigationSegue and SnapNavigationMediator in one.
//  Intended as an example. Not to be used or subclassed.
//
//  Use this approach to define a custom UIStoryboardSegue that uses an [already internally defined] Navigator and an internal mediation method when performing its transition.
//  To implement your own class, copy this class and:
//      - Use a custom unique Class name.
//      - In a matching storyboard segue, set the class of the segue instance to this class.
//      - Customize the mediation(source:destination:) method to dynamically craft the desired Navigation result.
//      - Optional: The navigationIntent.presentation can be set, which will override any segue presentation method.

import UIKit

class ExampleNavigationSegue: SnapNavigationSegue {

    override var mediation: (UIViewController, UIViewController) -> () {
        get {
            // Perform mediation here.
            return { source, destination in
            // A mediation might look like this:
            //                if let source = source as? ExpectedSourceSubclassOrProtocol,
            //                let destination = destination as? ExpectedDestinationSublassOrProtocol {
            //                    destination.valueToSet = source.providingValue
            //                }
            }
        }
        set {
            // Irrelevant.
        }
    }

}
Route navigator example
//  An example SnapRouteNavigator.
//  Intended as an example. Not to be used or subclassed.
//
//  Use this approach to define a custom Navigator that holds internal Navigation data mapped to Route enum cases.
//  To implement your own class, copy this class and:
//      - Use a custom unique Class name.
//      - Use a custom enum Route definition matching your navigation needs.
//      - Customize the `navigation<Route>(for:)` method to dynamically craft the desired Navigation result.
//
//  Example usage, from a UIViewController:
//      myNavigator = ExampleRouteNavigator(source: self)
//      myNavigator.navigate(using: ExampleRoute.presentSettings)



import UIKit

class ExampleRouteNavigator: SnapRouteNavigator {

    var navigation: SnapNavigation

    // MARK: - Initialization

    init(source: UIViewController) {
        navigation = SnapNavigation(source: source, destination: source)
    }

    // MARK: - Navigation

    func navigation<Route: CaseIterable>(for route: Route) -> SnapNavigation? {
        guard let route = route as? ExampleRoute else { return nil }
        switch route {
        case .presentSettings:
            // Set destination / destinationFactory here.
            // Set mediation here.
            // Set presentation here.
            return navigation
        case .showColleagueView(let viewData):
            // Set destination / destinationFactory here.
            // Set mediation here.
            // Set presentation here.
            return navigation
        case .showDetailView(let detailData):
            // Set destination / destinationFactory here.
            // Set mediation here.
            // Set presentation here.
            return navigation
        }
    }
}

enum ExampleRoute: CaseIterable {

    // Conformance to CaseIterable.
    static var allCases: [ExampleRoute] {
        return [
            .presentSettings,
            .showColleagueView(viewData: 0),
            .showDetailView(detailData: "")
        ]
    }

    case presentSettings
    case showColleagueView(viewData: Int)
    case showDetailView(detailData: String)
}

Introduction

View navigation in the context of the iOS UIKit framework is a process involving multiple objects and actions. Navigation starts with a trigger action, determining what is the starting source view controller and the resultant destination view controller. This is followed by presenting the destination view controller in a certain manner and completed by perfoming any desired transformations on the view controllers.

SnapNavigation is a behavioral pattern offering a unified interface for this navigation process. It aims to aid and improve upon standard navigation by realizing the following goals:

  • Composable, Modular, Customizable: Define each aspect of navigation separately; mix-and-match intent to targets as needed; mediation, and presentation can be performed by closure or delegate object, view instantiation can be performed by reference or factories.
  • Aggregate utility: Provide default presentation animation implementations for all common navigation triggers; handle entire navigation flow in one function call.
  • Separate tightly-bound concerns: Minimize colleague view references; separate the need to perform navigation actions from the actual implementation; view instantiation, mediation, and presentation can each be isolated.
  • Lightweight: Use as a library, not a framework; Promote usage by composition, not inheritance.
  • Flexible: Works for navigations triggered from code or as UIStoryboardSegue actions.

Navigation is described by model classes (SnapNavigation and concrete Route objects), with navigator classes managing the navigation actions (SnapNavigator, SnapNavigationRouter, and SnapNavigationSegue). Concrete navigation scenarios can be defined in your app partially or entirely as custom navigation models, sent to navigator object navigate methods dynamically, or mixtures of each approach as needed.

SnapNavigation: The data model describing navigation

A SnapNavigation class instance describes a particular navigation action.

SnapNavigation has four elementary attributes: source, destination, mediation, and presentation. Each of these is an enumeration. Collectively these describe the who, what, and how of the navigation.

source is the representative view object from which the navigation emanates.

destination is the representative view object to which the navigation transitions towards.

mediation governs any transformative methods peformed on source and destination.

presentation governs transitional animation and resultant presentation of source and destination.

A navigation must describe a source and destination, but other attributes are optional.

A navigation with only a source and destination describes an abstract association.

A navigation with a mediation and no presentation describes a purely mediational relationship. For example, data marshalling between source and destination.

A navigation with a presentation and no mediation describes a purely presentational relationship. For example, this could be a method to present the destination modally over the source involving an animation.

A navigation with both mediation and presentation describes a complete navigation. mediation and presentation together can be defined in a NavigationIntent.

SnapNavigator: An object that performs a navigation

SnapNavigator is a protocol adopted by an object that performs navigations.

SnapNavigator has a robust set of default implementations such that objects implementing this protocol are not required to implement any methods, unless customized behavior is desired. Most navigation customization can be performed by manipulating SnapNavigation data.

There are a number of navigate methods provided, allowing for a wide variety of composable navigation actions. These methods fall into two categories: navigate using a navigationProvider, or navigate using only provided arguments. The navigate(using:) and navigate(from:…) methods perform navigation using only provided arguments, and all other navigate methods are based on using navigationProvider data.

SnapNavigationSegue: A UIStoryboardSegue performing a custom navigation

SnapNavigationSegue is a base UIStoryboardSegue class implementing a navigation intent.

This is intended to be subclassed. A subclass with no custom implementations will behave as a standard UIStoryboardSegue. Custom UIStoryboardSegue classes are intended to handle custom presentation scenarios. Using a SnapNavigationSegue allows for full navigation customization (mediation and presentation), moving all navigation code into the segue class and removing the requirement to handle navigation in the UIViewController invoking the navigation in a prepare(for:) method.

To provide a custom mediation in your subclass, simply override mediation. This is the primary intended use case.

Alternatively, or in conjunction with mediation override, intent, mediator, or presentation can be overriden. Setting mediator gets precedence over mediation. A set presentation takes precedence over the internal UIStoryboardSegue presentation method triggered in the perform function.

As an alternative to custom dedicated subclasses, a dependency injection container can be used to set the desired navigation intent values. Such a framework must work with the storyboard instantiation lifecycle to properly set the values when this object is created but before the perform method is triggered.

SnapNavigationSegue will not work for embed segues, nor will it handle unwind segue methods. Neither of these segue types work by instantiating a UIStoryboardSegue. This is as intended, since the focus of embed and unwind segue actions are considered contained responsibilities of the view controller. An embed segue describes an relationship between a parent view controller and a child view controller, in which setup of the child view controller should be handled in the parent prepare(for:) method. An unwind segue acts on the established object hierarchy graph of the storyboard where the target view controller of the unwind should express capability to handle the unwind through a custom @IBAction function. If desired, both embed and unwind can be composed together in an extension to organize navigation code.

Routes: Powerful convenience for particular navigations

Routes are expressed as a CaseIterable types. Routes define concrete navigation use cases. They allow numerous specific navigation needs to be expressed from an object triggering a navigation, decoupling the navigation implementation details. A SnapNavigationRouter maps a given route to a SnapNavigation usable by a SnapNavigator.

Routes defined as enumerations with associated values can be used to provide conditional intent (e.g. mediation payload) as long they conform to CaseIterable, in conjuntion with a concrete SnapNavigationRouter class navigation function switch.