(翻译) iOS架构:探索 RIBs

原创作者:Stan Ostrovskiy
原文链接:iOS Architecture: Exploring RIBs
原文翻译:Grabin

Uber 的移动端开发架构细节

RIBs是什么?

加入Uber是我的iOS工程生涯的新篇章,所有这一切都始于称为RIBs的新架构。该体系结构背后的主要思想是:应用程序应该由业务逻辑来驱动的,而不是视图。理解这个思想的最佳方法是把它想象成一棵树:每个RIB都是一个节点,并且它可以一个或多个子节点,也有可能一个节点都没有。

RIBs tree



在应用程序的整个生命周期内,RIBs 可以添加或者分离,创建子节点,并和它交互,RIBs 代表了 “Router、Interactor、Builder”

  • Router 负责相邻 RIBs 之间的导航
  • Interactor 是处理RIB业务逻辑的主要组件。它对用户交互做出反应,与后端API沟通,并准备将要显示给用户的数据。
  • Builder是一个将所有RIB片段组合在一起的构造函数

还有一个可选的 ViewPresenter。View本身没有任何业务逻辑,它仅负责呈现UI,并用户触摸传递给 Interactor。Interactor拥有 View,并且该 View 通过委托模式与Interactor对话。Presenter 基本上就是定义了协议,这些协议是由 View 实现的。

例如,在“View”上点击 “Login” 按钮将触发 Interactor 中的Web任务,并且Interactor 将告诉 Presenter 显示活动指示器。Login 调用成功后,Interactor 将告诉 Router 导航到下一个页面。

这是一个简单的概述,现在我们可以深入研究RIB的每个组件,并了解它们如何协同工作。

进入RIBs

幸运的事情是,我们不必每次想使用所有组件创建新的RIB时都编写样板代码。我们可以安装和配置Xcode模板。要创建一个新的RIB,只需打开一个文件创建菜单,然后从列表中选择RIB:

RIBs template for Xcode.png



接下来我们会新建一个 RIB,把它命名为 Login,检查一下,它应该会拥有一个 View.

image.png



Xcode模板生成4个文件。我们将仔细研究它们,并讨论它们的功能。

image.png


LoginBuilder

众所周知,Builder负责创建所有 RIB 组件。
请注意,以下所有代码都是由Xcode模板自动生成的。

import RIBs

protocol LoginDependency: Dependency {
    // TODO: Declare the set of dependencies required by this RIB, but cannot be
    // created by this RIB.
}

final class LoginComponent: Component<LoginDependency> {

    // TODO: Declare 'fileprivate' dependencies that are only used by this RIB.
}

// MARK: - Builder
protocol LoginBuildable: Buildable {
    func build(withListener listener: LoginListener) -> LoginRouting
}

final class LoginBuilder: Builder<LoginDependency>, LoginBuildable {

    override init(dependency: LoginDependency) {
        super.init(dependency: dependency)
    }

    func build(withListener listener: LoginListener) -> LoginRouting {
        let component = LoginComponent(dependency: dependency)
        let viewController = LoginViewController()
        let interactor = LoginInteractor(presenter: viewController)
        interactor.listener = listener
        return LoginRouter(interactor: interactor, viewController: viewController)
    }
}



可能你先会注意到的点就是,里面大部分的结构都是协议,而不是具体的类。这是RIB的主要功能之一,我们将在本文后面讨论。

LoginDependency 用于将依赖项从其父项注入 RIB。例如,我们有一个webService用于执行登录Web请求。我们创建一个我们要注入的 WebServicing 协议:

protocol WebServicing: class {
    func login(userName: String, password: String, handler: (Result<String, Error>) -> Void)
}



现在我们可以更新 LoginDependency 协议,为 builder 提供对其依赖项:

protocol LoginDependency: Dependency {
    var webService: WebServicing { get }
}



这里的下一个组成部分是 LoginComponent。我们可以声明一些我们只能在当前Builder 中使用的局部变量,例如配置或者 AdMob ID等。在我们的示例中,我们将保留此类,因为我们不需要任何私有依赖项。

下一个协议是 LoginBuildable,它只有一个方法 build(with listener:)。这里会将父listener作为参数的注入进来使用。我们可以自由地向此构建方法添加更多参数,如果符合逻辑的话。

LoginBuilder 类实现了 LoginBuildable,它是此处的主要组成部分。它使用LoginDependency 创建一个 LoginComponent
LoginComponent 现在封装了我们为此RIB需要的所有依赖项。该 Builder 还创建一个 LoginViewController LoginInteractor,用于创建和返回 LoginRouter.

这是另外一行重要的代码:

interactor.listener = listener


这就是我们将父 Interactor 与子 Interactor 连接的方式。例如,我们有一个与 RootRIB 连接的 LoginRIB。在这种情况下,RootInteractor将必须实现LoginInteractor listener 声明的方法。如果 LoginInteractor 调用 dismissLogin,则 RootRIB 将实现此方法,分离 Login 这个 flow 并显示一个页面。



等我们需要使用 Router 的依赖的时候, 我们将 return Router,现在我们转到下一个组件 Interactor

LoginInteractor

想再次说明一下,下面所有的代码都是由Xcode模板自动生成的。

import RIBs
import RxSwift

protocol LoginRouting: ViewableRouting {
    // TODO: Declare methods the interactor can invoke to manage sub-tree via the router.
}

protocol LoginPresentable: Presentable {
    var listener: LoginPresentableListener? { get set }
    // TODO: Declare methods the interactor can invoke the presenter to present data.
}

protocol LoginListener: class {
    // TODO: Declare methods the interactor can invoke to communicate with other RIBs.
}

final class LoginInteractor: PresentableInteractor<LoginPresentable>, LoginInteractable, LoginPresentableListener {

    weak var router: LoginRouting?
    weak var listener: LoginListener?

    // TODO: Add additional dependencies to constructor. Do not perform any logic
    // in constructor.
    override init(presenter: LoginPresentable) {
        super.init(presenter: presenter)
        presenter.listener = self
    }

    override func didBecomeActive() {
        super.didBecomeActive()
        // TODO: Implement business logic here.
    }

    override func willResignActive() {
        super.willResignActive()
        // TODO: Pause any business logic.
    }
}



LoginRouting 是我们用来从 Login RIB 导航到后续 RIB 的协议。 假设我们希望能够导航到 CreateAccount 页面:

protocol LoginRouting: ViewableRouting {
    func routeToCreateAccount()
}



LoginPresentable 用于响应在 Interactor 中执行的业务逻辑来更新 Login 视图。 如果打开 LoginViewController,您会注意到它实现了此协议。

LoginPresentable 还拥有一个 LoginPresentableListener。 这是LoginViewController与 Interactor 进行通信并调用业务逻辑的一种方式。 换句话说,这是 Interactor 和 ViewController 相互通信的方式

image.png



如上所述,我们希望 ViewController 在执行Web任务时显示活动指示器。 为了实现这一点,我们向 LoginPresentable 添加了一个新方法 showActivityIndicator

protocol LoginPresentable: Presentable {
    var listener: LoginPresentableListener? { get set }
    func showActivityIndicator(_ isLoading: Bool)
}



最终,我们有一个 LoginListener。 还记得 LoginBuilder 中的这一行代码吗?

interactor.listener = listener



这是 Root RIB 将要去实现的 listener。是子级RIB与父级进行通信的一种方式。登录完成后,我们需要通知Root RIB,可以去取消登录流程了:

protocol LoginListener: class {
    func dismissLoginFlow()
}



现在我们看一下 LoginInteractor 类。 它有两个弱变量,routerlistener。 这就是 Interactor 分别连接到其 router 和 父Interactor 的方式。 可以看到,该 Interactor 还拥有一个 presenter。

我们应该记得,RIB 背后的核心思想是该应用程序应由业务逻辑驱动。 Interactor 是此业务逻辑所在的地方。



这是我们使用 interactor 控制应用程序流程的方式:

  • 调用 presenter 的方法来更新登录UI(示例中有showActivityIndicator)
  • 调用 router 的方法导航到子RIB(示例中有routeToCreateAccount)
  • 调用 listener 的方法与父RIB对话(示例中有dismissLoginFlow)

接下来,我们看到一些生命周期管理的方法 didBecomeActivewillResignActive。 这些方法是不言自明的,我们不会直接调用它们。 例如,我们可以在 didBecomeActive 中执行Web任务以获取所需的数据,或者根据我们的业务逻辑进行初始视图设置。

稍后我们将会回到 interactor,现在让我们结束其余的组成部分 —— router, view, 和 presenter。

LoginRouter

同样,Xcode模板会自动为您生成以下所有代码。

import RIBs

protocol LoginInteractable: Interactable {
    var router: LoginRouting? { get set }
    var listener: LoginListener? { get set }
}

protocol LoginViewControllable: ViewControllable {
    // TODO: Declare methods the router invokes to manipulate the view hierarchy.
}

final class LoginRouter: ViewableRouter<LoginInteractable, LoginViewControllable>, LoginRouting {
    
    // TODO: Constructor inject child builder protocols to allow building children.
    override init(interactor: LoginInteractable, viewController: LoginViewControllable) {
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
}



LoginInteractable是此处的主要协议,包含两个组成部分,LoginRoutingLoginListener。 我们在 Interactor 中都创建了它们。

LoginViewControllable 用于操纵视图层次结构。 因此,当Interactor告诉 router 使用 LoginRouting导航到 CreateAccount 时,router 最终需要显示CreateAccount屏幕。 我们需要添加以下方法:

final class LoginRouter: ViewableRouter<LoginInteractable, LoginViewControllable>, LoginRouting {
    
    override init(interactor: LoginInteractable, viewController: LoginViewControllable) {
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
    
    func routeToCreateAccount() {
        
    }
}



在展示其viewController之前,我们需要拥有 CreateAccount RIB。 所以这个时候我们先创建另一个RIB。

image.png


我们不会在此RIB中进行任何更改,因此只需将其保留并返回 LoginRouter 即可。

要构建CreateAccount RIB,LoginRouter 需要具有 CreateAccountBuilder。 声明一个类型为 CreateAccountBuildable 的私有变量,并更新 LoginRouter init,注入 CreateAccountBuildable

final class LoginRouter: ViewableRouter<LoginInteractable, LoginViewControllable>, LoginRouting {
    
    private let createAccountBuilder: CreateAccountBuildable
    
    init(
        interactor: LoginInteractable,
        viewController: LoginViewControllable,
        createAccountBuilder: CreateAccountBuildable
    ) {
        self.createAccountBuilder = createAccountBuilder
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
    
    func routeToCreateAccount() {
        
    }
}


记住,我们没有使用具体的类型CreateAccountBuilder。 相反,我们使用协议CreateAccountBuildable



现在我们可以完成routeToCreateAccount方法。

func routeToCreateAccount() {
    let router = createAccountBuilder.build(withListener: interactor)
    attachChild(router)
    viewController.present(router.viewControllable)
}


  1. 使用createAccountBuilder构建一个createAccountRouter。 我们需要在build方法中将当前的 interactor 作为 listener 传递。
  2. 将createAccountRouter作为子级附加到当前 Router。这就是我们构建RIB树的方式。
  3. 我们调用LoginViewControllable方法来呈现CreateAccount视图控制器。

在这里会注意到的第一件事是以下编译器错误:

Argument type ‘LoginInteractable’ does not conform to expected type ‘CreateAccountListener’

要解决此问题,我们需要确保LoginInteractable实现CreateAccountListener协议:

protocol LoginInteractable: Interactable, CreateAccountListener {
    var router: LoginRouting? { get set }
    var listener: LoginListener? { get set }
}



另一个要记住的是:我们使用 attachChild 方法附加 createAccountRouter。仔细想想,最终将需要另一种方法来关闭 CreateAccount 屏幕。 取消子屏幕后,我们必须将其 Router 与当前树分离。

当viewController不再可用,但相应的RIB仍在树中时,我们不想看到程序处于这种状态。 这最终可能导致内存泄漏和意外行为。

为了避免这种情况,我们将保留对CreateAccountRouter的引用。 在LoginRouter中创建一个变量:

final class LoginRouter: ViewableRouter<LoginInteractable, LoginViewControllable>, LoginRouting {
    
    private let createAccountBuilder: CreateAccountBuildable
    private let createAccountRouter: CreateAccountRouting?
    
    // ...
}



现在,让我们更新 routeToCreateAccount方法。 我们需要将 createAccountRouter 保存到本地变量。 另外,如果已经创建了子 Router,我们就可以防止自己创建 Router 和 present子视图控制器:

func routeToCreateAccount() {
    guard createAccountRouter == nil else { return }
    
    let router = createAccountBuilder.build(withListener: interactor)
    createAccountRouter = router
    attachChild(router)
    viewController.present(router.viewControllable)
}



最后,当我们要dismiss CreateAccount屏幕时,在使用视图层次结构进行操作后,我们必须分离其Router:

func detachCreateAccount() {
    guard let createAccountRouter = createAccountRouter else { return }
    createAccountRouter.viewControllable.uiviewController.dismiss(animated: true, completion: nil)
    detachChild(createAccountRouter)
    self.createAccountRouter = nil
}



Xcode将显示另一个编译器错误,因此我们需要更新 LoginBuilder 并将 CreateAccountBuilder 传递给 router init。 我们使用LoginBuilder创建并注入一个子 builder:

final class LoginBuilder: Builder<LoginDependency>, LoginBuildable {
    
    override init(dependency: LoginDependency) {
        super.init(dependency: dependency)
    }
    
    func build(withListener listener: LoginListener) -> LoginRouting {
        let component = LoginComponent(dependency: dependency)
        let viewController = LoginViewController()
        let interactor = LoginInteractor(presenter: viewController)
        interactor.listener = listener
        
        let createAccountBuilder = CreateAccountBuilder(dependency: component.dependency)
        
        return LoginRouter(
            interactor: interactor,
            viewController: viewController,
            createAccountBuilder: createAccountBuilder
        )
    }
}



请注意,我们使用component.dependency 作为 CreateAccountBuilder依赖项。 为此,我们需要LoginDependency来实现CreateAccountDependency协议。 这是我们将依赖关系从父RIB连接到子RIB的方式:

protocol LoginDependency: CreateAccountDependency {
    var webService: WebServicing { get }
}



在我们的示例中,CreateAccountDependency没有任何变量。 如果是这样,我们不得不在某些时候提供它们。 在根组件中创建并保留所有依赖项,然后使用此协议继承传递它们,这很方便。 我们将在本文结尾处进行此操作。

在这一点上,该应用程序应该编译没有任何错误。

LoginPresenter/LoginViewController

import RIBs
import RxSwift
import UIKit

protocol LoginPresentableListener: class {
    // TODO: Declare properties and methods that the view controller can invoke to perform
    // business logic, such as signIn(). This protocol is implemented by the corresponding
    // interactor class.
}

final class LoginViewController: UIViewController, LoginPresentable, LoginViewControllable {
    
    weak var listener: LoginPresentableListener?
}



LoginPresentableListener具有良好的自动生成的文档。 我们只需要知道我们要在此ViewController上执行哪些操作即可。 我们将向LoginPresentableListener添加两个方法:

protocol LoginPresentableListener: class {
    func didTapLogin(username: String, password: String)
    func didTapCreateAccount()
}



我们不会专注于UI,但是如果您希望在实际操作中看到它,则可以继续创建一个简单的UI。 确保按钮触发正确的 listener 方法。
LoginViewController类实现了我们之前配置的LoginPresentable协议(以便 interactor 可以与viewController通信)。 这意味着LoginViewController必须实现showActivityIndicator方法:

final class LoginViewController: UIViewController, LoginPresentable, LoginViewControllable {
    
    weak var listener: LoginPresentableListener?
    
    // MARK: - LoginPresentable
    
    func showActivityIndicator(_ isLoading: Bool) {
        
    }
}



viewController实现的下一个协议是LoginViewControllable(以便 router 可以修改视图层次结构)。 为了符合要求,LoginViewController必须实现当前方法:

final class LoginViewController: UIViewController, LoginPresentable, LoginViewControllable {
    
    weak var listener: LoginPresentableListener?
    
    // MARK: - LoginPresentable
    
    func showActivityIndicator(_ isLoading: Bool) {
        
    }
    
    // MARK: - LoginViewControllable
    
    func present(_ viewController: ViewControllable) {
        present(viewController.uiviewController, completion: nil)
    }
}


现在,这是我们在LoginViewController中需要做的所有事情。 同样,您可以添加缺少的UI按钮,文本字段和 loading状态的控件。

因为我们向LoginPresentableListener添加了一些方法,并且LoginInteractor实现了此协议,所以我们需要向 interactor 添加缺少的方法:

final class LoginInteractor: PresentableInteractor<LoginPresentable>, LoginInteractable, LoginPresentableListener {
    
    // ...
    
    // MARK: - LoginPresentableListener
    
    func didTapLogin(username: String, password: String) {
        
    }
    
    func didTapCreateAccount() {
        
    }
}



didTapCreateAccount必须路由到CreateAccount RIB,所以我们只需要调用现有的LoginRouting方法:

func didTapCreateAccount() {
    router?.routeToCreateAccount()
}



要调用登录Web任务,我们需要访问我们之前创建的WebServicing登录方法。 我们将把WerServicing传递给LoginInteractor init:

final class LoginInteractor: PresentableInteractor<LoginPresentable>, LoginInteractable, LoginPresentableListener {
    
    // ...
    
    private let webService: WebServicing
    
    init(presenter: LoginPresentable, webService: WebServicing) {
        self.webService = webService
        super.init(presenter: presenter)
        presenter.listener = self
    }
    
    // ...
}



在 interactor 中具有WebServicing,我们可以完成登录方法:

func didTapLogin(username: String, password: String) {
    presenter.showActivityIndicator(true)
    webService.login(userName: username, password: password) { [weak self] result in
        self?.presenter.showActivityIndicator(false)
        switch result {
        case let .success(userID):
            // do something with userID if needed
            self?.listener?.dismissLoginFlow()
        case let .failure(error):
            // log error
        }
    }
}



在此方法内,我们实现所有登录业务逻辑,显示和隐藏loading状态的控件,在登录成功时关闭LoginFlow,并在登录失败的情况下记录错误。 我们还添加另一个LoginPresentable方法showErrorAlert,如果登录失败,该方法将通知用户:

protocol LoginPresentable: Presentable {
    var listener: LoginPresentableListener? { get set }
    func showActivityIndicator(_ isLoading: Bool)
    func showErrorAlert()
}



编译器将确保您已在LoginViewController中实现此方法。 从登录失败的情况下调用此方法:


webService.login(userName: username, password: password) { [weak self] result in
    self?.presenter.showActivityIndicator(false)
    switch result {
    case let .success(userID):
        // do something with userID if needed
        self?.listener?.dismissLoginFlow()
    case let .failure(error):
        // log error
        self?.presenter.showErrorAlert()
    }
}



最后,我们必须更新LoginBuilder并将WebServicing依赖项传递到LoginInteractor中:

final class LoginBuilder: Builder<LoginDependency>, LoginBuildable {
    
    override init(dependency: LoginDependency) {
        super.init(dependency: dependency)
    }
    
    func build(withListener listener: LoginListener) -> LoginRouting {
        let component = LoginComponent(dependency: dependency)
        let viewController = LoginViewController()
        let interactor = LoginInteractor(presenter: viewController, webService: component.dependency.webService)
        interactor.listener = listener
        
        let createAccountBuilder = CreateAccountBuilder(dependency: component.dependency)
        
        return LoginRouter(
            interactor: interactor,
            viewController: viewController,
            createAccountBuilder: createAccountBuilder
        )
    }
}


Top-level RIB

现在,我们为应用程序提供了完整的登录模块。 如果您想查看全部内容,则必须添加一些缺失的部分。

创建一个Root RIB,它将成为Login RIB的父级(您应该能够使用上面提供的相同步骤将登录名连接到root。一些区别将在RootRouter和RootBuilder中,因为它是一个顶级RIB, 没有父类。

除了创建RootRouting,我们还需要创建LaunchRouting(为顶级RIB设计的特定RIB组件):

import RIBs

protocol RootDependency: Dependency {
}

final class RootComponent: Component<RootDependency> {
    
    private let rootViewController: RootViewController
    
    init(dependency: RootDependency,
         rootViewController: RootViewController) {
        self.rootViewController = rootViewController
        super.init(dependency: dependency)
    }
}

// MARK: - Builder

protocol RootBuildable: Buildable {
    func build() -> LaunchRouting
}

final class RootBuilder: Builder<RootDependency>, RootBuildable {
    
    override init(dependency: RootDependency) {
        super.init(dependency: dependency)
    }
    
    func build() -> LaunchRouting {
        let viewController = RootViewController()
        let component = RootComponent(
            dependency: dependency,
            rootViewController: viewController
        )
        
        let interactor = RootInteractor(presenter: viewController)
        
        return RootRouter(
            interactor: interactor,
            viewController: viewController
        )
    }
}



这是一个非常具体的案例。

RootRouter还将继承自LaunchRouting而不是ViewableRouter,后者是特定于启动的Router协议:

final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {
    override init(interactor: RootInteractable, viewController: RootViewControllable) {
        super.init(interactor: interactor, viewController: viewController)
        interactor.router = self
    }
}



我们还需要创建一个AppComponent,它使用具有EmptyDependency的组件。 该组件将具有我们要使用依赖协议传递的大多数依赖。 您可以创建一个继承自WebServicing协议的WebService类,并将其保留为AppComponent中的变量:

final class AppComponent: Component<EmptyDependency>, RootDependency {
    
}



在AppDelegate中,我们需要使用此AppComponent创建一个RootRouter,并在当前窗口中对其进行启动:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    private var launchRouter: LaunchRouting?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        
        let launchRouter = RootBuilder(dependency: AppComponent()).build()
        self.launchRouter = launchRouter
        launchRouter.launch(from: window)
        
        return true
    }
}



在这一点上,我们应该能够编译并启动该应用程序。 如果添加缺少的UI,则可以看到它的实际效果。

高级 RIBs

Mock 生成实体类

我在本文开头提到,在RIBs中,我们不使用具体类型,而是在大多数组件和依赖项中使用协议。 当我们想用单元测试覆盖我们的代码时,这非常方便。 由于RIBs中的所有业务逻辑都存在于Interactor中,因此我们尝试达到对 Interactor 和 Router 100%的测试覆盖率。 协议允许我们模拟我们使用的大多数类型,从而可以在不暴露实际类型的情况下对其进行测试。

但是同时,模拟协议是繁琐的工作,需要大量样板代码。 幸运的是,有多种工具可让我们生成协议的所有模拟。 其中之一是称为Mockolo的工具。 您可以单击提供的链接并安装依赖项,当然也可以随时使用其他模拟生成工具。 使用Mockolo,您要做的就是用///@ mockable注释标记协议并运行模拟生成。

例如,我们有一个要在测试中使用的WebServicing协议。 让我们为此服务生成模拟:

class WebServicingMock: WebServicing {
    init() { }
    
    var loginCallCount = 0
    var loginHandler: ((String, String, (Result<String, Error>) -> Void) -> ())?
    func login(username: String, password: String, handler: (Result<String, Error>) -> Void) {
        loginCallCount += 1
        if let loginHandler = loginHandler {
            loginHandler(username, password, handler)
        }
        
    }
}



这个 mock类 具有一个loginCallCount和loginHandler,我们将使用它们来测试是否调用了Login方法,以及它是否使用了正确的参数和结果。
我们可以为我们所有的RIB协议和依赖项生成模拟,并为广泛的单元测试范围打开它。

UnitTests

我将提供一个示例,说明如何使用mock生成通过测试覆盖LoginInteractor。

让我们看一下LoginInteractor中的 didTapLogin(:,:) 方法。 这是我们要测试的多种想法:

  • 让presenter显示 loading状态的控件
  • 让webService登录Web任务
  • 如果登录任务成功,则侦听器应调用dismissLoginFlow方法
  • 如果登录任务失败,则演示者应调用showErrorAlert方法
  • Web任务完成时,presenter 隐藏 loading状态的控件

这是将所有测试组件连接在一起的LoginInteractorTests的初始设置(模拟是由Mockolo生成的):

final class LoginInteractorTests: XCTestCase {
    private var interactor: LoginInteractor!
    private var presenter = LoginPresentableMock()
    private var listener = LoginListenerMock()
    private var router = LoginRoutingMock()
    private let webService = WebServicingMock()
    
    override func setUp() {
        super.setUp()
        
        interactor = LoginInteractor(presenter: presenter, webService: webService)
        router.viewControllable = ViewControllableMock()
        router.interactable = InteractableMock()
        
        interactor.router = router
        interactor.listener = listener
    }
}



让我们为didTapLogin方法编写测试。

func test_didTapLogin_triggersLoginWebTask_andEnableActivityIndicator() {
    presenter.showActivityIndicatorHandler = { isLoading in
        XCTAssertTrue(isLoading)
    }
    
    interactor.didTapLogin(username: "username", password: "password")
    
    XCTAssertEqual(webService.loginCallCount, 1)
    XCTAssertEqual(presenter.showActivityIndicatorCallCount, 1)
}

func test_loginSucceeded_invokesListenerDismissLoginFlow() {
    
    webService.loginHandler = { username, login, handler in
        return handler(.success("userID"))
    }
    
    interactor.didTapLogin(username: "username", password: "password")
    
    XCTAssertEqual(listener.dismissLoginFlowCallCount, 1)
    XCTAssertEqual(presenter.showActivityIndicatorCallCount, 2)
}

func test_loginFailed_invokesPresenterShowErrorAlert() {
    
    webService.loginHandler = { username, login, handler in
        return handler(.failure(WebServiceError.generic))
    }
    
    interactor.didTapLogin(username: "username", password: "password")
    
    XCTAssertEqual(presenter.showErrorAlertCallCount, 1)
    XCTAssertEqual(presenter.showActivityIndicatorCallCount, 2)
}


同样,我们可以涵盖其余的Interactor方法,包括使用我们的didBecome active方法。 router 可以用相同的方式进行测试。因为在RIB中,我们将大多数组件作为协议,而不是具体类型。 此外,router 和 Interactor 都大多包含实现其他协议的方法。 使用mock生成,我们无需编写任何其他代码即可使用单元测试覆盖所有应用业务逻辑。

依赖注入

在示例项目中,我们使用Dependency和Component处理依赖关系,并且必须从AppComponent一直传递下去。 拥有协议继承可以使之清晰明了,井井有条,但是连接所有依赖项仍然很繁琐。

我们使用了另一个开源的Uber工具:Needle Dependency Injection

在这里,我不会详细解释Needle,但是上面的链接提供了很好的解释,并提供了有关如何集成和使用它的示例。

总结

在本文中,我介绍了RIB体系结构的要点,解释了一些极端情况,并提供了其大多数组件的提示和示例。

对于这个小项目,RIB看起来像是过分杀伤,就像我们在示例中使用的那样。 但是,如果您了解这些基础知识,那么采用这种架构不会花费太多时间或精力。 而且,如果将其与依赖项注入和模拟生成相结合,将为大多数应用程序用例提供一个大胆的解决方案。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349