Giter VIP home page Giter VIP logo

routerservice's Introduction

Router Service

RouterService is a navigation/routing/dependency injection framework where all navigation is done through Routes. Based on the system used at Airbnb presented at BA:Swiftable 2019.

RouterService is meant to be as a dependency injector for modular apps where each targets contains an additional "interface" target. The linked article contains more info about that, but as a summary, this starts from the principle that a feature target should never directly depend on another one. Instead, a feature only has access to another feature's interface, which contains RouterService's Route objects. RouterService then takes care of executing this Route and injecting the necessary dependencies.

The final result is:

  • An app with a horizontal dependency graph (very fast build times)
  • Dynamic navigation (any screen can be pushed from anywhere)

How RouterService Works

Alongside a interfaced modular app, the RouterService framework attempts to improve build times by completely severing the connections between ViewControllers. This is how a RouterService app operates:

  • You have a Dependency protocol, which can be any class instance needed by anyone at any point. (additional parameters like a screen's "context" are not dependencies)
  • A Feature is a class that creates instances of a ViewController, given a list of said dependencies. They are not public. Here's an example:

Target: HTTPClientInterface, who imports RouterServiceInterface:

import RouterServiceInterface

protocol HTTPClientProtocol: Dependency {}

Target: HTTPClient, who imports HTTPClientInterface:

import HTTPClientInterface

class HTTPClient: HTTPClientProtocol { ... }

Target: Profile, who imports HTTPClientInterface and RouterServiceInterface, but not their concrete targets:

import HTTPClientInterface
import RouterServiceInterface

enum ProfileFeature: Feature {
    struct Dependencies {
        let client: HTTPClientProtocol
        let routerService: RouterServiceProtocol
    }

    static var dependenciesInitializer: AnyDependenciesInitializer {
        return AnyDependenciesInitializer(Dependencies.init)
    }

    static func build(
        dependencies: ProfileFeature.Dependencies,
        fromRoute route: Route?
    ) -> UIViewController {
        return ProfileViewController(...)
    }
}

Because the feature is isolated from the concrete client and other features, changes made to these targets will not recompile the Profile target, as the real instances will be injected in runtime by the RouterService. That's a big build time improvement!

Routes

Instead of pushing features by directly creating instances of their ViewControllers, in RouterService, the navigation is done completely through Routes. By themselves, Routes are just Codable structs that can hold contextual information about an action (like the previous screen that triggered this route, for analytics purposes). However, the magic comes from how they are used: Routes are paired with RouteHandlers: classes that define a list of supported Routes and a method that returns which Feature should be pushed when a certain Route is executed. For example, to expose the ProfileFeature shown above to the rest of the app, the hypothetical Profile target could expose routes through its interface, and define a public ProfileRouteHandler like this:

Target: ProfileInterface, which depends on RouterServiceInterface:

struct ProfileRoute: Route {
    static let identifier: String = "profile_mainroute"
    let analyticsContext: String
}

Target: Profile, as seen before, but now also depending on ProfileInterface:

import ProfileInterface
import RouterServiceInterface

public final class ProfileRouteHandler: RouteHandler {
    public var routes: [Route.Type] {
        return [ProfileRoute.self]
    }

    public func destination(
        forRoute route: Route,
        fromViewController viewController: UIViewController
    ) -> AnyFeature {
        guard route is ProfileRoute else {
            preconditionFailure()
        }
        return AnyFeature(ProfileFeature.self)
    }
}

Now, to push a new Feature, all a Feature has to do is import its next feature's interface target and call the navigate(_:) method from the RouterServiceProtocol (which is accessible through the Feature's dependencies -- note how it is added as a dependency in the ProfileFeature example), sending the desired Route to be navigated to.

import SomeLoginInterface

let loginRoute = SomeLoginRouteFromTheLoginFeature()
dependencies.routerService.navigate(
    toRoute: loginRoute,
    fromView: self,
    presentationStyle: Push(),
    animated: true
)

Again, as Profile does not directly imports the concrete SomeLogin target, changes made to it will not recompile Profile unless the interface itself is changed.

Tying everything up

If all features are isolated, how do you start the app?

While the features are isolated from the other targets, you should have a "main" target that imports everything and everyone. From there, you can create a concrete instance of your RouterService and register everyone's RouteHandlers and Dependencies.

import HTTPClient
import Profile
import Login

class AppDelegate {

   let routerService = RouterService()

   func didFinishLaunchingWith(...) {
       routerService.register(dependency: HTTPClient(), forType: HTTPClientProtocol.self)

       routerService.register(routeHandler: ProfileRouteHandler())
       routerService.register(routeHandler: LoginRouteHandler())

       //Your usual UIWindow stuff
       //...
       window.rootViewController = routerService.navigationController(withInitialFeature: ProfileFeature.self)

       return true
   }
}

For more information and examples, check the example app provided inside this repo.

AnyRoute

All Routes are Codable, but what if more than one route can be returned by the backend?

For this purpose, RouterServiceInterface provides a type-erased AnyRoute that can decode any registered Route from a specific string format. This allows you to have your backend dictate how navigation should be handled inside the app.

AnyRoute is Decodable, so you should first add it to your backend's response model:

struct ProfileResponse: Decodable {
    let title: String
    let route: AnyRoute
}

But before decoding ProfileResponse, you need to inject a RouterService that contains the desired routes registered into the JSONDecoder() by using the relevant method from RouterServiceProtocol:

let decoder = JSONDecoder()

routerService.injectContext(toDecoder: decoder)

decoder.decode(ProfileResponse.self, from: data)

The string format expected by the framework is a string in the route_identifier|parameters_json_string. For example, to decode the ProfileRoute shown in the beginning of this README, ProfileResponse should look like this:

{
    "title": "Profile Screen",
    "route": "profile_mainroute|{\"analyticsContext\": \"Home\"}"
}

Installation

CocoaPods

pod 'RouterService'

routerservice's People

Contributors

rockbruno avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.