作者 | Stan Ostrovskiy 来源 | Medium The Startup
加入 UBER 是我的 iOS 工程师职业的新篇章,所有这一切都始于称为 RIB
的新架构。该架构背后的主要思想是,应用程序应由业务逻辑而不是视图驱动。展示 RIB
的最佳方法是一棵树:每个 RIB
都是一个节点,并且它可以不包含子节点,也可以包括一个或多个子节点。
在应用程序生命周期中,可以附加和分离 RIB
,创建子节点并与之交互。
RIB 即 “路由 + 交互器 + 构造器 (Router Interactor Builder)”。
• 路由(Router) 负责相邻 RIB
之间的导航
• 交互器(Interactor) 是处理 RIB
业务逻辑的主要组件。它响应用户交互,与后端对话,并准备数据显示给用户。
• 构造器(Builder) 是一个将所有 RIB
片段组合在一起的构造器。
还有一个可选的 视图(View) 和 展示器(Presenter)。 View
本身没有任何业务逻辑,它仅负责呈现 UI 并接受传递给 Interactor
的用户交互。 Interactor
拥有 View
,并且 View
通过委托模式与 Interactor
对话。 Presenter
基本上是 View
实现的协议。
例如,在 View
上点击“登录”按钮将触发 Interactor
中的 Web 任务, Interactor
将告诉 Presenter
显示活动指示器。登录成功后, Interactor
将告诉 Router
导航到下一个页面。
这是一个简单的概述,现在我们可以深入研究 RIB
的每个组件,并了解它们如何协同工作。
注:以下统一使用
Router
,Interactor
,Builder
,View
和Presenter
来描述这 5 个组件。
幸运的是,在你想要创建一个带有所有组件的 RIB
时,不需要手写所有样板代码。您可以安装和配置 Xcode 模板。要创建新的 RIB
,只需打开文件创建菜单,然后从列表中选择 RIB
:
我们将创建一个名为 Login
的 RIB
,并勾选 Owns corresponding view
以让 RIB
带有视图:
这个 Xcode
模板会生成 4 个文件。我们接下来会仔细研究它们中的每一个,并讨论它们的功能。
一个 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(withListener:)
。这里的 listener 参数是父侦听器。我们可以自由地向此构建方法添加更多参数,只需要根据需求来定制。
LoginBuilder
类实现了 LoginBuildable
协议,它是这里的主要组件。它使用 LoginDependency
创建一个 LoginComponent
。
LoginComponent
现在封装了这个 RIB
需要的所有依赖项。该构建器还创建一个 LoginViewController
和 LoginInteractor
,用于创建和返回 LoginRouter
。
这里是另一行重要的代码:
interactor.listener = listener
这就是我们将父
Interactor
与子Interactor
连接的方式。例如,我们有一个与RootRIB
连接的LoginRIB
。在这种情况下,RootInteractor
必须实现LoginInteractor
侦听器将声明的方法。如果LoginInteractor
有一个dismissLogin
方法,则根RIB
将实现此方法以分离 Login 流并显示主页。
稍后,当我们需要使用 Router
的某些依赖项时,我们将返回到 Router
,现在我们来看看下一个组件 Interactor
。
同样, 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
相互通信的方式:
如上所述,我们希望我们的视图控制器在执行 Web 任务时显示活动指示器。为了实现这一点,我们在 LoginPresentable
中添加了一个新方法 showActivityIndicator
:
protocol LoginPresentable: Presentable {
var listener: LoginPresentableListener? { get set }
func showActivityIndicator(_ isLoading: Bool)
}
最后,我们有一个 LoginListener
。还记得 LoginBuilder
中的这一行代码吗?
interactor.listener = listener
这是 Root RIB
将要实现的侦听器。这是子级 RIB
与父级进行通信的一种方式。登录完成后,我们需要通知 Root RIB
,以便可以取消登录流程:
protocol LoginListener: class {
func dismissLoginFlow()
}
现在我们看一下 LoginInteractor
类。它有两个 weak 变量: router
和 listener
。这就是 Interactor
连接到其 Router
和父 Interactor
的方式。可以看到,该 Interactor
还拥有一个 Presenter
。
回顾一下,RIB 背后的核心思想是该应用程序应由业务逻辑驱动。
Interactor
就是此业务逻辑所在的地方。
这里是我们使用 Interactor
控制应用程序流程的方式:
• 我们调用 presenter
方法来更新登录 UI(我们的示例中有 showActivityIndicator)
• 我们调用 router
方法导航到子 RIB(我们的示例中有 routeToCreateAccount)
• 我们调用 listener
方法与父 RIB 对话(我们的示例中有 dismissLoginFlow)
接下来,我们可以看到一些生命周期方法 didBecomeActive
和 willResignActive
。这些方法是自解释的,我们不会直接调用它们。例如,我们可以在 didBecomeActive
中执行Web任务以获取所需的数据,或者根据我们的业务逻辑进行初始视图设置。
稍后我们将返回到 Interactor
,现在让我们来完成其余的组件 -- Router
, View
和 Presenter
同样,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
是这里的主要协议,包含两个组件, LoginRouting
和 LoginListener
。我们在 Interactor
中创建它们。
LoginViewControllable
用于操纵视图层次结构。因此,当 Interactor
告诉 Router
使用 LoginRouting
导航到 CreateAccount
时, Router
最终将需要显示 CreateAccount
页面。我们需要添加以下方法:
protocol LoginViewControllable: ViewControllable {
func present(_ viewController: ViewControllable)
}
如您所见, LoginRouter
实现了 LoginRouting
协议,因此我们需要添加必需的方法 routeToCreateAccount
:
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
。
我们不会在此 RIB
中进行任何更改,因此只需将其保留并返回 LoginRouter
即可。
要构建 CreateAccount RIB
, LoginRouter
需要有一个 CreateAccountBuilder
。声明一个类型为 CreateAccountBuildable
的私有变量,并更新 LoginRouter
构造器,以注入 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)
}
• 我们使用 createAccountBuilder
构建一个 createAccountRouter
。我们需要在 Build
方法中将当前的 Interactor
作为侦听器传递。
• 我们将 createAccountRouter
作为子级附加到当前 Router
。这就是我们构建 RIB
树的方式。
• 我们调用 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
和提供子视图控制器:
func routeToCreateAccount() {
guard createAccountRouter == nil else { return }
let router = createAccountBuilder.build(withListener: interactor)
createAccountRouter = router
attachChild(router)
viewController.present(router.viewControllable)
}
最后,当我们要关闭 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
的构造器。我们使用 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
没有任何变量。如果有的话,我们需要在某些时候提供它们。在根组件中创建并保留所有依赖项,然后使用此协议继承传递它们,这很方便。我们将在本文结尾处进行此操作。
到目前为止,该应用程序应该编译没有任何错误。
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 按钮,文本字段和活动指示器。
因为我们向 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
登录方法。我们将把 WebServicing
传递给 LoginInteractor
构造器:
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
}
// ...
}
在 Interator
中有 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
}
}
}
在此方法内部,我们实现了所有登录业务逻辑,显示和隐藏活动指示器,在登录成功时关闭 LoginFlow
页面,并在登录失败的情况下记录错误。我们还添加另一个 LoginPresentable
方法 showErrorAlert
,如果登录失败,该方法将通知用户:
protocol LoginPresentable: Presentable {
var listener: LoginPresentableListener? { get set }
func showActivityIndicator(_ isLoading: Bool)
func showErrorAlert()
}
编译器将确保您已在 LoginViewController
中实现此方法。在 login
失败的情况下调用此方法:
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
)
}
}
现在,我们为应用程序提供了完整的登录模块。如果您想查看全部内容,则必须添加一些缺失的部分。
创建一个 Root RIB
,它将成为 Login RIB
的父级(您应该能够使用上面提供的相同步骤将登录连接到 root。主要的区别是在 RootRouter
和 RootBuilder
中,因为它是一个顶级 RIB,所以没有父 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
,后者是特定于启动的路由协议:
final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {
override init(interactor: RootInteractable, viewController: RootViewControllable) {
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
}
我们还需要创建一个 AppComponent
,它继承自具有 EmptyDependency
泛型类型的 Component
。该组件有我们希望使用依赖协议传递的大多数依赖。您可以创建一个继承自 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,则可以看到它的实际效果。
我在本文开头提到,在 RIB 中,我们不使用具体类型,而是在大多数组件和依赖项中使用协议。当我们想用单元测试覆盖我们的代码时,这非常方便。由于 RIB
中的所有业务逻辑都存在于 Interactor
中,因此我们尝试将 Interactor
和 Router
测试覆盖率达到 100%的。协议允许我们模拟我们使用的大多数类型,从而可以在不暴露实际类型的情况下对其进行测试。
但是同时,mock 协议是繁琐的工作,需要大量样板代码。幸运的是,有多种工具可让我们生成协议的所有 mock 代码。其中包括 Mockolo 工具 (https://github.com/uber/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 协议和依赖项生成 mock 代码。
我将提供一个示例,说明如何使用 mock generation
通过测试覆盖 LoginInteractor
。
让我们看一下 LoginInteractor
中的 didTapLogin(:_:)
方法。以下是我们要测试的几个点:
• Presenter
显示活动指示器
• webService
构建一个登录 Web 任务
• 如果登录任务成功,则侦听器应调用 dismissLoginFlow
方法
• 如果登录任务失败,则 Presenter
应调用 showErrorAlert
方法
• Web任务完成时, Presenter
隐藏活动指示器
这是将所有测试组件连接在一起的 LoginInteractorTests
的初始设置(mock 是由 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
方法,包括使用我们的 didBecomeActive
方法。 Router
可以用相同的方式进行测试。这是可能的,因为在 RIB 中,我们将大多数组件作为协议,而不是具体类型。此外, Router
和 Interactor
都大多包含实现其他协议的方法。使用 mock generation
,我们无需编写任何其他代码即可使用单元测试覆盖所有应用业务逻辑。
在示例项目中,我们使用 Dependency
和 Component
处理依赖关系,并且必须从 AppComponent
传递所有方法。拥有协议继承可以使之清晰明了,井井有条,但是连接所有依赖项仍然很繁琐。
我们使用了另一个开源的 Uber 工具: Needle Dependency Injection
。
我不会在这里详细解释 Needle,但是上面的链接提供了很好的解释,并提供了有关如何集成和使用它的示例。
如果我看到读者对此感兴趣,那么我将在以后的文章中介绍 Needle,所以请不要忘记关注我:)
在本文中,我介绍了 RIBs
体系结构的要点,解释了一些极端情况,并提供了其大多数组件的使用和示例。
对于这个小项目,RIB 看起来像是过度设计,就像我们在示例中使用的那样。但是,如果您了解这些基础知识,那么采用这种架构不会花费太多时间或精力。而且,如果将其与依赖项注入和 mock generation
相结合,则将为大多数应用程序用例提供一个大胆的解决方案。
您可以在此处找到带有UI和所有必需逻辑的完整示例项目 https://github.com/Stan-Ost/RIBsTutorialExample。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。