-
Notifications
You must be signed in to change notification settings - Fork 906
iOS Tutorial 4
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.
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
.
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
}
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
}
}
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)
}
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, ()))
}
Copyright © 2017 Uber Technologies Inc.
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.