Skip to content

iOS Tutorial 2

Tuomas Artman edited this page Oct 27, 2017 · 29 revisions

RIB Tutorial 2: Composing RIBs

Goals of this exercise

The main goals of this exercise to understand the following concepts:

  • Child RIB calling up to parent RIB.
  • Attaching/detaching a child RIB when the parent decides so.
  • Creating a view-less RIB.
    • Cleanup view modifications when view-less RIB is detached.
  • Attaching a child RIB when the parent RIB first loads up.
    • Lifecycle of a RIB.
  • Unit testing.

Inform Root RIB from LoggedOut RIB that the players have logged in

  1. Update LoggedOutListener to add a method that allows LoggedOut RIB to inform Root RIB that players did login.
protocol LoggedOutListener: class {
    func didLogin(withPlayer1Name player1Name: String, player2Name: String)
}
  1. Update LoggedOutInteractor's implementation of login method to perform the business logic of handling nil player names, as well as calling to LoggedOutListener to inform Root RIB that players did 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
    }
}

Attach view-less LoggedIn RIB and detach LoggedOut RIB on button tap

  1. Delete DELETE_ME.swift, it was only required to stub out classes you're about to implement.
  2. Update RootRouting in RootInteractor.swift to add a method to route to LoggedIn RIB.
protocol RootRouting: ViewableRouting {
    func routeToLoggedIn(withPlayer1Name player1Name: String, player2Name: String)
}
  1. Invoke RootRouting in RootInteractor to route to LoggedIn, as the LoggedOutListener implementation.
func didLogin(withPlayer1Name player1Name: String, player2Name: String) {
    router?.routeToLoggedIn(withPlayer1Name: player1Name, player2Name: player2Name)
}
  1. Create LoggedIn RIB using Xcode templates as a view-less RIB.
    1. Uncheck the "Owns corresponding view" box.
  2. Pass LoggedInBuildable protocol into RootRouter via constructor injection.
    init(interactor: RootInteractable,
         viewController: RootViewControllable,
         loggedOutBuilder: LoggedOutBuildable,
         loggedInBuilder: LoggedInBuildable) {
        self.loggedOutBuilder = loggedOutBuilder
        self.loggedInBuilder = loggedInBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
    1. Update the RootBuilder to instantiate LoggedInBuilder concrete class and inject into RootRouter.
      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)
      }
    2. For now just pass RootComponent as the dependency for LoggedInBuilder. We'll cover what this means in tutorial3.
    3. RootRouter only uses and depends on LoggedInBuildable, instead of the concrete LoggedInBuilder class. This allows us to mock the LoggedInBuildable when unit testing RootRouter. This is a constraint by 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.
  3. Implement the routeToLoggedIn method in RootRouter.
    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)
    }
    1. We need to first detach the LoggedOutRouter and dismiss its view. This means we need to add a new method in RootViewControllable, which is the protocol for our current scope, Root.
      protocol RootViewControllable: ViewControllable {
          func present(viewController: ViewControllable)
          func dismiss(viewController: ViewControllable)
      }
    2. Once we add the dismiss method. We are then required to provide an implementation in RootViewController.
      func dismiss(viewController: ViewControllable) {
          if presentedViewController === viewController.uiviewController {
              dismiss(animated: true, completion: nil)
          }
      }
    3. Then we can go back to RootRouter routeToLoggedIn method and build the LoggedInRouter.
    4. And finally attach the LoggedInRouter.
    5. Notice we don't need to call our RootViewControllable to show the LoggedIn RIB, since LoggedIn RIB is view-less. We do need to show the LoggedOut in the routeToLoggedOut method.

Unit test RootRouter

Now that Root is complete. Let’s unit test its router. The same process can be applied to unit test other parts of a RIB.

  1. Create a swift file in TicTacToeTests/Root and call it RootRouterTests and put it in the TicTacToeTest test bundle.
  2. 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.

Pass in LoggedInViewControllable instead of creating it

Because LoggedIn RIB does not own its own view, yet it still needs to be able to show child RIBs views, we need one of its ancestors, in this case the parent Root RIB to provide the view.

  1. Update RootViewController to conform to LoggedInViewControllable.
// MARK: LoggedInViewControllable

extension RootViewController: LoggedInViewControllable {

}
  1. Dependency inject the LoggedInViewControllable protocol. Don't worry about what this means or how it's done for now. This is the focus area for tutorial3. We'll revisit this portion in that tutorial. For now, override LoggedInBuilder.swift with this code.

Now LoggedIn RIB can show/hide its child RIBs views by invoking methods on LoggedInViewControllable.

Attach OffGame RIB on LoggedIn didLoad

  1. Create an OffGame RIB that displays a "Start Game" button.
    1. This is the same as creating the LoggedOut RIB in the previous tutorial. Feel free to use the provided OffGameViewController implementation to save time.
    2. Use the Xcode RIB template to create a RIB that owns its own view. Then paste in the provided OffGameViewController.
  2. 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
    }
    1. 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)
      }
    2. For now just pass RootComponent as the dependency for LoggedInBuilder. We'll cover what this means in the tutorial3.
  3. 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)
}
  1. Invoke attachOffGame in didLoad method of LoggedInRouter.
override func didLoad() {
    super.didLoad()
    attachOffGame()
}

Cleanup LoggedIn attached views when LoggedIn is detached

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.

  1. Add a dismiss method to the LoggedInViewControllable protocol.
    protocol LoggedInViewControllable: ViewControllable {
        func present(viewController: ViewControllable)
        func dismiss(viewController: ViewControllable)
    }
    1. Similar to other protocol declarations, this declares that LoggedIn RIB needs the functionality of dismissing a ViewControllable.
  2. Dismiss the currentChild's view controller in cleanupViews method.
    func cleanupViews() {
        if let currentChild = currentChild {
            viewController.dismiss(viewController: currentChild.viewControllable)
        }
    }
    1. This method is invoked from the LoggedInInteractor when it resigns active.

Attach TicTacToe RIB and detach OffGame RIB on button tap

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.

Attach OffGame RIB and detach TicTacToe RIB when we have a winner

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.

  1. Declare routeToOffGame in LoggedInRouting protocol.
protocol LoggedInRouting: Routing {
    func cleanupViews()
    func routeToTicTacToe()
    func routeToOffGame()
}
  1. Implement gameDidEnd method in LoggedInInteractor.
// MARK: - TicTacToeListener

func gameDidEnd() {
    router?.routeToOffGame()
}
  1. Implement routeToOffGame in LoggedInRouter.
func routeToOffGame() {
    detachCurrentChild()
    attachOffGame()
}
private func detachCurrentChild() {
    if let currentChild = currentChild {
        detachChild(currentChild)
        viewController.dismiss(viewController: currentChild.viewControllable)
    }
}

Solution

The completed solution is at tutorial3 folder.

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