-
Notifications
You must be signed in to change notification settings - Fork 912
iOS Tutorial 2
Note: If you haven't completed tutorial 1 yet, we encourage you to do so before jumping into this tutorial.
Welcome to the RIBs tutorials, which have ben 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 TicTacToe game using the RIBs architecture and associated tooling.
For this tutorial, we'll use the source code here as a starting point. Follow the README to install and open the project before reading any further.
The main goals of this exercise to understand the following concepts:
- Having a child RIB calling up to its parent RIB.
- Attaching/detaching a child RIB when the parent interactor decides to do so.
- Creating a view-less RIB.
- Cleaning up view modifications when a viewless RIB is detached.
- Attaching a child RIB when the parent RIB first loads up.
- Understanding the lifecycle of a RIB.
- Unit testing a RIB.
We want to inform the Root RIB from LoggedOut RIB when the players have logged in. For this we need to implement the following code.
First, update the LoggedOutListener
(in the LoggedOutInteractor.swift file) to add a method that allows LoggedOut RIB to inform Root RIB that players has logged in.
protocol LoggedOutListener: class {
func didLogin(withPlayer1Name player1Name: String, player2Name: String)
}
This forces the any parent RIB of the LoggedOut RIB to implement the didLogin
function and makes sure that the compiler enforces the contract between the parent and its children.
Add a login method implementation to the LoggedOutInteractor
and have it perform the business logic of handling nil player names, as well as calling to LoggedOutListener to inform its parent (the Root RIB in our project) that the players have login.
// MARK: - LoggedOutPresentableListener
func login(withPlayer1Name player1Name: String?, player2Name: String?) {
let player1NameWithDefault = playerName(player1Name, withDefaultName: "Player 1")
let player2NameWithDefault = playerName(player2Name, withDefaultName: "Player 2")
listener?.didLogin(withPlayer1Name: player1NameWithDefault, player2Name: player2NameWithDefault)
}
private func playerName(_ name: String?, withDefaultName defaultName: String) -> String {
if let name = name {
return name.isEmpty ? defaultName : name
} else {
return defaultName
}
}
Delete DELETE_ME.swift (in the LoggedIn group), it was only required to stub out classes you're about to implement.
Update RootRouting
in RootInteractor.swift to add a method to route to LoggedIn RIB.
protocol RootRouting: ViewableRouting {
func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String)
}
This establishes the contract between the RootInteractor
and its router, the RootRouter
.
Invoke RootRouting
in RootInteractor
to route to the LoggedIn RIB, by implementing the LoggedOutListener
protocol.
// MARK: - LoggedOutListener
func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
}
This will make the Root RIB route to the LoggedIn RIB whenever the LoggedOut RIB says that users have logged in.
Next, create a LoggedIn RIB using Xcode templates as a viewless RIB. Uncheck the "Owns corresponding view" box create the RIB in the LoggedIn group. There are already some files in that group, but don't worry about them. Also make sure that the newly created files are added to the TicTacToe target.
The RootRouter now needs to be able to build the newly created LoggedIn RIB. We make this possible by passing in the LoggedInBuildable protocol into the RootRouter (in the RootRouter.swift file) via constructor injection. Modify the constructor of the RootRouter to look like this:
init(interactor: RootInteractable,
viewController: RootViewControllable,
loggedOutBuilder: LoggedOutBuildable,
loggedInBuilder: LoggedInBuildable) {
self.loggedOutBuilder = loggedOutBuilder
self.loggedInBuilder = loggedInBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
You'll also need to add a private loggedInBuilder instance constant for the RootRouter:
// MARK: - Private
private let loggedInBuilder: LoggedInBuildable
...
Then, update the RootBuilder to instantiate a LoggedInBuilder concrete class and inject it into the RootRouter. Modify the build function of the RootBuilder (yes, in the RootBuilder.swift file) like so:
func build() -> LaunchRouting {
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)
return RootRouter(interactor: interactor,
viewController: viewController,
loggedOutBuilder: loggedOutBuilder,
loggedInBuilder: loggedInBuilder)
}
If you look at the code we just modified, we pass in RootComponent as the dependency for the LoggedInBuilder using constructor injection. Don't worry about why we do this rignt now, we'll this when we get to tutorial 3.
RootRouter
depends on LoggedInBuildable
, instead of the concrete LoggedInBuilder
class. This allows us to pass in a test mock for the LoggedInBuildable
when unit testing the RootRouter
. This is a constraint of Swift, where swizzling based mocking is not possible. At the same time, this also follows the protocol-based programming principle, ensuring RootRouter and LoggedInBuilder are not tightly coupled.
Now, lets implement the routeToLoggedIn
method in the RootRouter
(by now I'm sure you already know what file RootRouter
is implemented in). A good place to add it is just before the // MARK: - Private
section.
// MARK: - RootRouting
func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String) {
// Detach logged out.
if let loggedOut = self.loggedOut {
detachChild(loggedOut)
viewController.dismiss(viewController: loggedOut.viewControllable)
self.loggedOut = nil
}
let loggedIn = loggedInBuilder.build(withListener: interactor)
attachChild(loggedIn)
}
When routing to the LoggedIn RIB, we first detach the LoggedOutRouter
and dismiss its view. In order to be able to do so, we need to add a new method in the RootViewControllable
protocol.
Modify the protocol look like this:
protocol RootViewControllable: ViewControllable {
func present(viewController: ViewControllable)
func dismiss(viewController: ViewControllable)
}
Once we add the dismiss
method to the protocol, we are then required by the compiler to provide an implementation in the RootViewController
. Just add it under the present
method.
func dismiss(viewController: ViewControllable) {
if presentedViewController === viewController.uiviewController {
dismiss(animated: true, completion: nil)
}
}
Going back to the RootRouter
's routeToLoggedIn
, it can now correctly dismiss the loggedOut RIB. The last two lines of the routeToLoggedIn
method build the LoggedInRouter, and attach it as a child. Routers always attach the routers of their children.
Notice we don't need to call a RootViewControllable
to show the LoggedIn RIB, as the LoggedIn
RIB doesn't have a view. If you want to see what presenting a RIB with a view looks like, check out the routeToLoggedOut
method.
Since the LoggedIn RIB does not own its own view and yet still needs to be able to show the views of it's child RIBs, the LoggedIn RIB needs to use the view of its ancestor. In our case, the parent Root RIB to provide the view.
Update RootViewController
to conform to LoggedInViewControllable
, by adding the following snippet to the end of the implementation:
// MARK: LoggedInViewControllable
extension RootViewController: LoggedInViewControllable {
}
Now we need to dependency inject the LoggedInViewControllable protocol. We'll not walk you throught this right now, as this is the focus area for tutorial3. For now, just override the content of LoggedInBuilder.swift with this code.
Now the LoggedIn RIB can show and hide its child RIBs views by invoking methods on the LoggedInViewControllable
.
Create an OffGame RIB that displays a "Start Game" button. This is the same as creating the LoggedOut RIB in the previous tutorial. Feel free to use the provided OffGameViewController implementation to save time. Use the Xcode RIB template to create a RIB that owns its own view. Then paste in the provided OffGameViewController.
Pass in the OffGameBuildable protocol into LoggedInRouter via constructor injection. This is the same as how we just passed LoggedInBuildable into RootRouter.
init(interactor: LoggedInInteractable,
viewController: LoggedInViewControllable,
offGameBuilder: OffGameBuildable) {
self.offGameBuilder = offGameBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
Update the LoggedInBuilder to instantiate OffGameBuilder concrete class and inject into LoggedInRouter. This is the same as how we just instantiated LoggedInBuilder.
func build(withListener listener: LoggedInListener) -> LoggedInRouting {
let component = LoggedInComponent(dependency: dependency)
let interactor = LoggedInInteractor()
interactor.listener = listener
let offGameBuilder = OffGameBuilder(dependency: component)
return LoggedInRouter(interactor: interactor,
viewController: component.loggedInViewController,
offGameBuilder: offGameBuilder)
}
For now just pass RootComponent as the dependency for LoggedInBuilder. We'll cover what this means in the tutorial3.
Implement attachOffGame method in LoggedInRouter to build and attach OffGame RIB and present its view controller.
private var currentChild: ViewableRouting?
private func attachOffGame() {
let offGame = offGameBuilder.build(withListener: interactor)
self.currentChild = offGame
attachChild(offGame)
viewController.present(viewController: offGame.viewControllable)
}
Invoke attachOffGame in didLoad method of LoggedInRouter.
override func didLoad() {
super.didLoad()
attachOffGame()
}
Because LoggedIn RIB doesn't own its own view, but rather uses a protocol to modify the view hierarchy one of its ancestors, in this case Root, provided, when Root detaches LoggedIn, Root has no way to directly remove the view modifications LoggedIn may have performed. Fortunately, the Xcode templates we used to generate the view-less LoggedIn RIB already provides a hook for us to clean up the view modifications when LoggedIn is detached/deactivated.
Add a dismiss method to the LoggedInViewControllable protocol.
protocol LoggedInViewControllable: ViewControllable {
func present(viewController: ViewControllable)
func dismiss(viewController: ViewControllable)
}
Similar to other protocol declarations, this declares that LoggedIn RIB needs the functionality of dismissing a ViewControllable.
Dismiss the currentChild's view controller in cleanupViews method.
func cleanupViews() {
if let currentChild = currentChild {
viewController.dismiss(viewController: currentChild.viewControllable)
}
}
This method is invoked from the LoggedInInteractor when it resigns active.
This step is very similar to attaching LoggedIn RIB and detaching LoggedOut RIB when the "Login" button it tapped. To save time, the TicTacToe RIB is provided already. In order to route to TicTacToe, we should implement routeToTicTacToe in LoggedInRouter and wire up the button tap event from OffGameViewController to OffGameInteractor and then finally to LoggedInInteractor.
This is very similar to the other listener based routing we've already exercised. If you used the TicTacToe RIB provided above, a listener is already setup. We just need to implement it in the LoggedInInteractor.
Declare routeToOffGame in LoggedInRouting protocol.
protocol LoggedInRouting: Routing {
func cleanupViews()
func routeToTicTacToe()
func routeToOffGame()
}
Implement gameDidEnd method in LoggedInInteractor.
// MARK: - TicTacToeListener
func gameDidEnd() {
router?.routeToOffGame()
}
Implement routeToOffGame in LoggedInRouter.
func routeToOffGame() {
detachCurrentChild()
attachOffGame()
}
private func detachCurrentChild() {
if let currentChild = currentChild {
detachChild(currentChild)
viewController.dismiss(viewController: currentChild.viewControllable)
}
}
Finally, let's write some test for our app. Let's unit test our RootRouter
The same principles can be applied to unit testing other parts of a RIB, and there's even a tooling template that will create all the unit tests for a RIB for you.
Create a swift file in the TicTacToeTests/Root group and call it RootRouterTests. Add it to the TicTacToeTest bundle.
Let’s write a test that verifies when we invoke routeToLoggedIn
, the RootRouter
invokes the LoggedInBuildable
protocol and attaches the returned Router. Feel free to refer to the written test: RootRouterTests.swift.
You completed the second tutorial Now onwards to tutorial 3.
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.