Skip to content

iOS Tutorial 4

Pavel Mazurin edited this page Nov 11, 2017 · 16 revisions

Deep Linking Workflows

Note: If you haven't completed tutorial 3 yet, we encourage you to do so before jumping into this tutorial.

Welcome to the RIBs tutorials, which have been designed to give you a hands-on walkthrough through the core concepts of RIBs. As part of the tutorials, you'll be building a simple tic-tac-toe game using the RIBs architecture and associated tooling.

For tutorial 4, we'll start source code that can be found here. Follow the README to install and open the project before reading any further.

Goals

The goals of this tutorial are to learn the following:

  • Understand basics of RIB workflows
  • Learn how to create actionable item interfaces, implement their methods, and create workflows to launch specifics flows via deeplinks.

In the end, you should be able to open the app from Safari, by calling to the url ribs-training://launchGame?gameId=ticTacToe, which should start a game with the specified gameId.

Implement the url handler

In order for the application to handle a custom url scheme, we should add the following lines in the Info.plist:

<dict>
    <key>CFBundleURLName</key>
    <string>com.ubercab.Game</string>
    <key>CFBundleURLSchemes</key>
    <array>
        <string>ribs-training</string>
    </array>
</dict>

As a second step, we'll introduce a new protocol UrlHandler in the AppDelegate.swift:

protocol UrlHandler: class {
    func handle(_ url: URL)
}

Add an instance variable to the AppDelegate class:

private var urlHandler: UrlHandler?

And make sure that the application delegate passes url to the urlHandler:

public func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
    urlHandler?.handle(url)
    return true
}

The RootInteractor is going to be our UrlHandler. So we need to make RootInteractor to conform to the UrlHandler protocol:

final class RootInteractor: PresentableInteractor<RootPresentable>, 
    RootInteractable, 
    RootPresentableListener, 
    RootActionableItem, 
    UrlHandler

To handle a url we need to pass it to the LaunchGameWorkflow, and subscribe to the workflow. We can do that, since LaunchGameWorkflow's ActionableItem is RootActionableItem, and RootInteractor conforms to this protocol.

// MARK: - UrlHandler

func handle(_ url: URL) {
    let launchGameWorkflow = LaunchGameWorkflow(url: url)
    launchGameWorkflow
        .subscribe(self)
        .disposeOnDeactivate(interactor: self)
}

Let's change the RootBuilder, so that it returns UrlHandler together with RootRouting instance:

protocol RootBuildable: Buildable {
    func build() -> (launchRouter: LaunchRouting, urlHandler: UrlHandler)
}
func build() -> (launchRouter: LaunchRouting, urlHandler: UrlHandler) {
    let viewController = RootViewController()
    let component = RootComponent(dependency: dependency,
                                  rootViewController: viewController)
    let interactor = RootInteractor(presenter: viewController)

    let loggedOutBuilder = LoggedOutBuilder(dependency: component)
    let loggedInBuilder = LoggedInBuilder(dependency: component)
    let router = RootRouter(interactor: interactor,
                            viewController: viewController,
                            loggedOutBuilder: loggedOutBuilder,
                            loggedInBuilder: loggedInBuilder)

    return (router, interactor)
}

And set the urlHandler in the AppDelegate:

public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let window = UIWindow(frame: UIScreen.main.bounds)
    self.window = window

    let result = RootBuilder(dependency: AppComponent()).build()
    launchRouter = result.launchRouter
    urlHandler = result.urlHandler
    launchRouter?.launchFromWindow(window)

    return true
}

Implement the workflow

Let's implement the workflow in the Promo folder. We are assuming the promotion feature is the one that provides this workflow. Let's declare the Root scope actionable item we need in order to launch a game. For the Root scope, we need to wait until the players log in. In order to do that, we simply modify the RootActionableItem protocol in the ActionableItemsCore module:

public protocol RootActionableItem: class {
    func waitForLogin() -> Observable<(LoggedInActionableItem, ())>
}

The return type is Observable<(NextActionableItemType, NextValueType)>, which allows us to chain another step for the next actionable item with a new value. In our case here, once we are logged in, we are routed to the LoggedIn scope. Which means that NextActionableItemType is LoggedInActionableItem which we'll define in the next step. We don't need any values to process our workflow, so our NextValueType is just Void.

Once we get to the LoggedIn scope, we'll need to launch a game with an ID. So let's define the LoggedInActionableItem in a new file.

import RxSwift

public protocol LoggedInActionableItem: class {
    func launchGame(with id: String?) -> Observable<(LoggedInActionableItem, ())>
}

Now let's write our workflow in the Promo folder. All workflows should inherit from the Workflow base class. And since we start at the Root scope, initial actionable item type should be the RootActionableItem.

import RIBs
import RxSwift

public class LaunchGameWorkflow: Workflow<RootActionableItem> {
    public init(url: URL) {
        super.init()

        let gameId = parseGameId(from: url)

        self
            .onStep { (rootItem: RootActionableItem) -> Observable<(LoggedInActionableItem, ())> in
                rootItem.waitForLogin()
            }
            .onStep { (loggedInItem: LoggedInActionableItem, _) -> Observable<(LoggedInActionableItem, ())> in
                loggedInItem.launchGame(with: gameId)
            }
            .commit()
    }

    private func parseGameId(from url: URL) -> String? {
        let components = URLComponents(string: url.absoluteString)
        let items = components?.queryItems ?? []
        for item in items {
            if item.name == "gameId" {
                return item.value
            }
        }

        return nil
    }
}

Integrate the waitForLogin step at the Root scope

As part of the setup, our RootInteractor already conforms to the RootActionableItem protocol. In this section, we just need to make it compile by providing the necessary implementations.

Let's implement the new waitForLogin method in the RootInteractor. Each scope's interactor is always the actionable item for that scope.

Since the "wait for login" action is asynchronous, it's best we use Rx in this case. We first declare a ReplaySubject that holds the LoggedInActionableItem, in the RootInteractor. We use a ReplaySubject because once we are logged in, we don't want to wait for the "next" login.

private let loggedInActionableItemSubject = ReplaySubject<LoggedInActionableItem>.create(bufferSize: 1)

Next, we can simply return this subject as an Observable in our waitForLogin method. What this does is, as soon as we have a LoggedInActionableItem emitted from the Observable, our workflow's step of waiting for login is completed. Therefore, we can move onto the next step with the LoggedInActionableItem as our actionable item type.

// MARK: - RootActionableItem

func waitForLogin() -> Observable<(LoggedInActionableItem, ())> {
    return loggedInActionableItemSubject
        .map { (loggedInItem: LoggedInActionableItem) -> (LoggedInActionableItem, ()) in
            (loggedInItem, ())
        }
}

Finally, we need to emit the LoggedInActionableItem into the subject, when we route to logged in. We do this by modifying the didLogin method in the RootInteractor.

// MARK: - LoggedOutListener

func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
    let loggedInActionableItem = router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
    if let loggedInActionableItem = loggedInActionableItem {
        loggedInActionableItemSubject.onNext(loggedInActionableItem)
    }
}

As the didLogin method's new implementation suggests, we need to update the RootRouting protocol's routeToLoggedIn method to return the LoggedInActionableItem instance, which as mentioned before, should be the LoggedInInteractor.

protocol RootRouting: ViewableRouting {
    func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) -> LoggedInActionableItem
}

Now let's update the RootRouter implementation since the RootRouting protocol has been modified. We need to return the LoggedInActionableItem, which would be the LoggedInInteractor.

func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) -> LoggedInActionableItem {
    // Detach logged out.
    if let loggedOut = self.loggedOut {
        detachChild(loggedOut)
        viewController.replaceModal(viewController: nil)
        self.loggedOut = nil
    }

    let loggedIn = loggedInBuilder.build(withListener: interactor, player1Name: player1Name, player2Name: player2Name)
    attachChild(loggedIn.router)
    return loggedIn.actionableItem
}

As the new routeToLoggedIn implementation shows, we now need to update the LoggedInBuildable protocol so it returns a tuple of the LoggedInRouting and LoggedInActionableItem.

protocol LoggedInBuildable: Buildable {
    func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -> (router: LoggedInRouting, actionableItem: LoggedInActionableItem)
}

Since the LoggedInBuildable protocol has changed, we need to update the LoggedInBuilder implementation to conform to the changes. We just need to return back the interactor as well. As mentioned before, the interactor of a scope should be the actionable item for that scope.

func build(withListener listener: LoggedInListener, player1Name: String, player2Name: String) -> (router: LoggedInRouting, actionableItem: LoggedInActionableItem) {
    let component = LoggedInComponent(dependency: dependency,
                                      player1Name: player1Name,
                                      player2Name: player2Name)
    let interactor = LoggedInInteractor(games: component.games)
    interactor.listener = listener

    let offGameBuilder = OffGameBuilder(dependency: component)
    let router = LoggedInRouter(interactor: interactor,
                          viewController: component.loggedInViewController,
                          offGameBuilder: offGameBuilder)
    return (router, interactor)
}

Integrate the launchGame step at LoggedIn scope

Let's update the LoggedInInteractor to conform to the LoggedInActionableItem protocol that we declared earlier. Recall that each scope's interactor should always conform to the actionable item protocol for that scope.

final class LoggedInInteractor: Interactor, LoggedInInteractable, LoggedInActionableItem

We can then provide an implementation to conform to the LoggedInActionableItem protocol.

// MARK: - LoggedInActionableItem

func launchGame(with id: String?) -> Observable<(LoggedInActionableItem, ())> {
    let game: Game? = games.first { game in
        return game.id.lowercased() == id?.lowercased() 
    }

    if let game = game {
        router?.routeToGame(with: game.builder)
    }

    return Observable.just((self, ()))
}

Once you've read through the documentation, learn the core concepts of RIBs by running through the tutorials and use RIBs more efficiently with the platform-specific tooling we've built.

Tutorial 1

iOS, Android

Tutorial 2

iOS, Android

Tutorial 3

iOS, Android

Tutorial 4

iOS, Android

Tooling

iOS, Android

Clone this wiki locally