原創(chuàng)作者:Stan Ostrovskiy
原文鏈接:iOS Architecture: Exploring RIBs
原文翻譯:Grabin
Uber 的移動端開發(fā)架構(gòu)細(xì)節(jié)
RIBs是什么丰涉?
加入U(xiǎn)ber是我的iOS工程生涯的新篇章袁铐,所有這一切都始于稱為RIBs的新架構(gòu)辩尊。該體系結(jié)構(gòu)背后的主要思想是:應(yīng)用程序應(yīng)該由業(yè)務(wù)邏輯來驅(qū)動的,而不是視圖装获。理解這個(gè)思想的最佳方法是把它想象成一棵樹:每個(gè)RIB都是一個(gè)節(jié)點(diǎn)籍滴,并且它可以一個(gè)或多個(gè)子節(jié)點(diǎn)伙菜,也有可能一個(gè)節(jié)點(diǎn)都沒有甥角。
在應(yīng)用程序的整個(gè)生命周期內(nèi)垦搬,RIBs 可以添加或者分離呼寸,創(chuàng)建子節(jié)點(diǎn),并和它交互猴贰,RIBs 代表了 “Router对雪、Interactor、Builder”米绕。
- Router 負(fù)責(zé)相鄰 RIBs 之間的導(dǎo)航
- Interactor 是處理RIB業(yè)務(wù)邏輯的主要組件瑟捣。它對用戶交互做出反應(yīng),與后端API溝通栅干,并準(zhǔn)備將要顯示給用戶的數(shù)據(jù)迈套。
- Builder是一個(gè)將所有RIB片段組合在一起的構(gòu)造函數(shù)
還有一個(gè)可選的 View 和 Presenter。View本身沒有任何業(yè)務(wù)邏輯非驮,它僅負(fù)責(zé)呈現(xiàn)UI交汤,并用戶觸摸傳遞給 Interactor。Interactor擁有 View劫笙,并且該 View 通過委托模式與Interactor對話芙扎。Presenter 基本上就是定義了協(xié)議,這些協(xié)議是由 View 實(shí)現(xiàn)的填大。
例如戒洼,在“View”上點(diǎn)擊 “Login” 按鈕將觸發(fā) Interactor 中的Web任務(wù),并且Interactor 將告訴 Presenter 顯示活動指示器允华。Login 調(diào)用成功后圈浇,Interactor 將告訴 Router 導(dǎo)航到下一個(gè)頁面。
這是一個(gè)簡單的概述靴寂,現(xiàn)在我們可以深入研究RIB的每個(gè)組件磷蜀,并了解它們?nèi)绾螀f(xié)同工作。
進(jìn)入RIBs
幸運(yùn)的事情是百炬,我們不必每次想使用所有組件創(chuàng)建新的RIB時(shí)都編寫樣板代碼褐隆。我們可以安裝和配置Xcode模板。要?jiǎng)?chuàng)建一個(gè)新的RIB剖踊,只需打開一個(gè)文件創(chuàng)建菜單庶弃,然后從列表中選擇RIB:
接下來我們會新建一個(gè) RIB,把它命名為
Login
德澈,檢查一下歇攻,它應(yīng)該會擁有一個(gè) View.Xcode模板生成4個(gè)文件。我們將仔細(xì)研究它們梆造,并討論它們的功能缴守。
LoginBuilder
眾所周知,Builder負(fù)責(zé)創(chuàng)建所有 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)
}
}
可能你先會注意到的點(diǎn)就是,里面大部分的結(jié)構(gòu)都是協(xié)議鸡捐,而不是具體的類栈暇。這是RIB的主要功能之一,我們將在本文后面討論箍镜。
LoginDependency
用于將依賴項(xiàng)從其父項(xiàng)注入 RIB源祈。例如,我們有一個(gè)webService用于執(zhí)行登錄Web請求色迂。我們創(chuàng)建一個(gè)我們要注入的 WebServicing
協(xié)議:
protocol WebServicing: class {
func login(userName: String, password: String, handler: (Result<String, Error>) -> Void)
}
現(xiàn)在我們可以更新 LoginDependency
協(xié)議香缺,為 builder 提供對其依賴項(xiàng):
protocol LoginDependency: Dependency {
var webService: WebServicing { get }
}
這里的下一個(gè)組成部分是 LoginComponent
。我們可以聲明一些我們只能在當(dāng)前Builder
中使用的局部變量歇僧,例如配置或者 AdMob ID等图张。在我們的示例中,我們將保留此類诈悍,因?yàn)槲覀儾恍枰魏嗡接幸蕾図?xiàng)祸轮。
下一個(gè)協(xié)議是 LoginBuildable
,它只有一個(gè)方法 build(with listener:)
侥钳。這里會將父listener
作為參數(shù)的注入進(jìn)來使用适袜。我們可以自由地向此構(gòu)建方法添加更多參數(shù),如果符合邏輯的話舷夺。
LoginBuilder
類實(shí)現(xiàn)了 LoginBuildable
苦酱,它是此處的主要組成部分。它使用LoginDependency
創(chuàng)建一個(gè) LoginComponent
给猾。
LoginComponent
現(xiàn)在封裝了我們?yōu)榇薘IB需要的所有依賴項(xiàng)疫萤。該 Builder 還創(chuàng)建一個(gè) LoginViewController LoginInteractor
,用于創(chuàng)建和返回 LoginRouter
.
這是另外一行重要的代碼:
interactor.listener = listener
這就是我們將父 Interactor 與子 Interactor 連接的方式敢伸。例如给僵,我們有一個(gè)與 RootRIB 連接的 LoginRIB。在這種情況下详拙,RootInteractor將必須實(shí)現(xiàn)LoginInteractor listener 聲明的方法。如果 LoginInteractor 調(diào)用
dismissLogin
蔓同,則 RootRIB 將實(shí)現(xiàn)此方法饶辙,分離 Login 這個(gè) flow 并顯示一個(gè)頁面。
等我們需要使用 Router 的依賴的時(shí)候斑粱, 我們將 return
Router弃揽,現(xiàn)在我們轉(zhuǎn)到下一個(gè)組件 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 導(dǎo)航到后續(xù) RIB 的協(xié)議痕慢。 假設(shè)我們希望能夠?qū)Ш降?CreateAccount 頁面:
protocol LoginRouting: ViewableRouting {
func routeToCreateAccount()
}
LoginPresentable 用于響應(yīng)在 Interactor 中執(zhí)行的業(yè)務(wù)邏輯來更新 Login 視圖。 如果打開 LoginViewController
涌矢,您會注意到它實(shí)現(xiàn)了此協(xié)議掖举。
LoginPresentable 還擁有一個(gè) LoginPresentableListener
。 這是LoginViewController與 Interactor 進(jìn)行通信并調(diào)用業(yè)務(wù)邏輯的一種方式娜庇。 換句話說塔次,這是 Interactor 和 ViewController 相互通信的方式:
如上所述,我們希望 ViewController 在執(zhí)行Web任務(wù)時(shí)顯示活動指示器名秀。 為了實(shí)現(xiàn)這一點(diǎn)励负,我們向
LoginPresentable
添加了一個(gè)新方法 showActivityIndicator
:
protocol LoginPresentable: Presentable {
var listener: LoginPresentableListener? { get set }
func showActivityIndicator(_ isLoading: Bool)
}
最終,我們有一個(gè) LoginListener
匕得。 還記得 LoginBuilder
中的這一行代碼嗎继榆?
interactor.listener = listener
這是 Root RIB 將要去實(shí)現(xiàn)的 listener
。是子級RIB與父級進(jìn)行通信的一種方式汁掠。登錄完成后略吨,我們需要通知Root RIB,可以去取消登錄流程了:
protocol LoginListener: class {
func dismissLoginFlow()
}
現(xiàn)在我們看一下 LoginInteractor 類调塌。 它有兩個(gè)弱變量晋南,router
和 listener
。 這就是 Interactor 分別連接到其 router 和 父Interactor 的方式羔砾。 可以看到负间,該 Interactor 還擁有一個(gè) presenter。
我們應(yīng)該記得姜凄,RIB 背后的核心思想是該應(yīng)用程序應(yīng)由業(yè)務(wù)邏輯驅(qū)動政溃。 Interactor 是此業(yè)務(wù)邏輯所在的地方。
這是我們使用 interactor 控制應(yīng)用程序流程的方式:
- 調(diào)用 presenter 的方法來更新登錄UI(示例中有showActivityIndicator)
- 調(diào)用 router 的方法導(dǎo)航到子RIB(示例中有routeToCreateAccount)
- 調(diào)用 listener 的方法與父RIB對話(示例中有dismissLoginFlow)
接下來态秧,我們看到一些生命周期管理的方法 didBecomeActive
和 willResignActive
董虱。 這些方法是不言自明的,我們不會直接調(diào)用它們申鱼。 例如愤诱,我們可以在 didBecomeActive
中執(zhí)行Web任務(wù)以獲取所需的數(shù)據(jù),或者根據(jù)我們的業(yè)務(wù)邏輯進(jìn)行初始視圖設(shè)置捐友。
稍后我們將會回到 interactor淫半,現(xiàn)在讓我們結(jié)束其余的組成部分 —— 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是此處的主要協(xié)議昏滴,包含兩個(gè)組成部分,LoginRouting 和 LoginListener对人。 我們在 Interactor 中都創(chuàng)建了它們谣殊。
LoginViewControllable 用于操縱視圖層次結(jié)構(gòu)。 因此牺弄,當(dāng)Interactor告訴 router 使用 LoginRouting導(dǎo)航到 CreateAccount 時(shí)姻几,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鲜棠。 所以這個(gè)時(shí)候我們先創(chuàng)建另一個(gè)RIB。
我們不會在此RIB中進(jìn)行任何更改培慌,因此只需將其保留并返回 LoginRouter 即可豁陆。
要構(gòu)建CreateAccount RIB,LoginRouter 需要具有 CreateAccountBuilder吵护。 聲明一個(gè)類型為 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。 相反瓮恭,我們使用協(xié)議CreateAccountBuildable
現(xiàn)在我們可以完成routeToCreateAccount方法雄坪。
func routeToCreateAccount() {
let router = createAccountBuilder.build(withListener: interactor)
attachChild(router)
viewController.present(router.viewControllable)
}
- 使用createAccountBuilder構(gòu)建一個(gè)createAccountRouter。 我們需要在build方法中將當(dāng)前的 interactor 作為 listener 傳遞屯蹦。
- 將createAccountRouter作為子級附加到當(dāng)前 Router维哈。這就是我們構(gòu)建RIB樹的方式。
- 我們調(diào)用LoginViewControllable方法來呈現(xiàn)CreateAccount視圖控制器登澜。
在這里會注意到的第一件事是以下編譯器錯(cuò)誤:
Argument type ‘LoginInteractable’ does not conform to expected type ‘CreateAccountListener’
要解決此問題阔挠,我們需要確保LoginInteractable實(shí)現(xiàn)CreateAccountListener協(xié)議:
protocol LoginInteractable: Interactable, CreateAccountListener {
var router: LoginRouting? { get set }
var listener: LoginListener? { get set }
}
另一個(gè)要記住的是:我們使用 attachChild
方法附加 createAccountRouter
。仔細(xì)想想脑蠕,最終將需要另一種方法來關(guān)閉 CreateAccount 屏幕购撼。 取消子屏幕后,我們必須將其 Router 與當(dāng)前樹分離谴仙。
當(dāng)viewController不再可用迂求,但相應(yīng)的RIB仍在樹中時(shí),我們不想看到程序處于這種狀態(tài)晃跺。 這最終可能導(dǎo)致內(nèi)存泄漏和意外行為揩局。
為了避免這種情況,我們將保留對CreateAccountRouter的引用哼审。 在LoginRouter中創(chuàng)建一個(gè)變量:
final class LoginRouter: ViewableRouter<LoginInteractable, LoginViewControllable>, LoginRouting {
private let createAccountBuilder: CreateAccountBuildable
private let createAccountRouter: CreateAccountRouting?
// ...
}
現(xiàn)在谐腰,讓我們更新 routeToCreateAccount
方法。 我們需要將 createAccountRouter
保存到本地變量涩盾。 另外十气,如果已經(jīng)創(chuàng)建了子 Router,我們就可以防止自己創(chuàng)建 Router 和 present子視圖控制器:
func routeToCreateAccount() {
guard createAccountRouter == nil else { return }
let router = createAccountBuilder.build(withListener: interactor)
createAccountRouter = router
attachChild(router)
viewController.present(router.viewControllable)
}
最后春霍,當(dāng)我們要dismiss CreateAccount屏幕時(shí)砸西,在使用視圖層次結(jié)構(gòu)進(jìn)行操作后,我們必須分離其Router:
func detachCreateAccount() {
guard let createAccountRouter = createAccountRouter else { return }
createAccountRouter.viewControllable.uiviewController.dismiss(animated: true, completion: nil)
detachChild(createAccountRouter)
self.createAccountRouter = nil
}
Xcode將顯示另一個(gè)編譯器錯(cuò)誤址儒,因此我們需要更新 LoginBuilder 并將 CreateAccountBuilder 傳遞給 router init芹枷。 我們使用LoginBuilder創(chuàng)建并注入一個(gè)子 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依賴項(xiàng)莲趣。 為此鸳慈,我們需要LoginDependency來實(shí)現(xiàn)CreateAccountDependency協(xié)議。 這是我們將依賴關(guān)系從父RIB連接到子RIB的方式:
protocol LoginDependency: CreateAccountDependency {
var webService: WebServicing { get }
}
在我們的示例中喧伞,CreateAccountDependency沒有任何變量走芋。 如果是這樣,我們不得不在某些時(shí)候提供它們潘鲫。 在根組件中創(chuàng)建并保留所有依賴項(xiàng)翁逞,然后使用此協(xié)議繼承傳遞它們,這很方便溉仑。 我們將在本文結(jié)尾處進(jìn)行此操作挖函。
在這一點(diǎn)上,該應(yīng)用程序應(yīng)該編譯沒有任何錯(cuò)誤浊竟。
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上執(zhí)行哪些操作即可。 我們將向LoginPresentableListener添加兩個(gè)方法:
protocol LoginPresentableListener: class {
func didTapLogin(username: String, password: String)
func didTapCreateAccount()
}
我們不會專注于UI逐沙,但是如果您希望在實(shí)際操作中看到它哲思,則可以繼續(xù)創(chuàng)建一個(gè)簡單的UI。 確保按鈕觸發(fā)正確的 listener 方法吩案。
LoginViewController類實(shí)現(xiàn)了我們之前配置的LoginPresentable協(xié)議(以便 interactor 可以與viewController通信)棚赔。 這意味著LoginViewController必須實(shí)現(xiàn)showActivityIndicator方法:
final class LoginViewController: UIViewController, LoginPresentable, LoginViewControllable {
weak var listener: LoginPresentableListener?
// MARK: - LoginPresentable
func showActivityIndicator(_ isLoading: Bool) {
}
}
viewController實(shí)現(xiàn)的下一個(gè)協(xié)議是LoginViewControllable(以便 router 可以修改視圖層次結(jié)構(gòu))。 為了符合要求徘郭,LoginViewController必須實(shí)現(xiàn)當(dāng)前方法:
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)
}
}
現(xiàn)在靠益,這是我們在LoginViewController中需要做的所有事情。 同樣残揉,您可以添加缺少的UI按鈕胧后,文本字段和 loading狀態(tài)的控件。
因?yàn)槲覀兿騆oginPresentableListener添加了一些方法抱环,并且LoginInteractor實(shí)現(xiàn)了此協(xié)議壳快,所以我們需要向 interactor 添加缺少的方法:
final class LoginInteractor: PresentableInteractor<LoginPresentable>, LoginInteractable, LoginPresentableListener {
// ...
// MARK: - LoginPresentableListener
func didTapLogin(username: String, password: String) {
}
func didTapCreateAccount() {
}
}
didTapCreateAccount必須路由到CreateAccount RIB纸巷,所以我們只需要調(diào)用現(xiàn)有的LoginRouting方法:
func didTapCreateAccount() {
router?.routeToCreateAccount()
}
要調(diào)用登錄Web任務(wù),我們需要訪問我們之前創(chuàng)建的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
}
}
}
在此方法內(nèi),我們實(shí)現(xiàn)所有登錄業(yè)務(wù)邏輯竖伯,顯示和隱藏loading狀態(tài)的控件存哲,在登錄成功時(shí)關(guān)閉LoginFlow,并在登錄失敗的情況下記錄錯(cuò)誤七婴。 我們還添加另一個(gè)LoginPresentable方法showErrorAlert祟偷,如果登錄失敗,該方法將通知用戶:
protocol LoginPresentable: Presentable {
var listener: LoginPresentableListener? { get set }
func showActivityIndicator(_ isLoading: Bool)
func showErrorAlert()
}
編譯器將確保您已在LoginViewController中實(shí)現(xiàn)此方法打厘。 從登錄失敗的情況下調(diào)用此方法:
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依賴項(xiàng)傳遞到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
現(xiàn)在,我們?yōu)閼?yīng)用程序提供了完整的登錄模塊婚惫。 如果您想查看全部內(nèi)容氛赐,則必須添加一些缺失的部分。
創(chuàng)建一個(gè)Root RIB先舷,它將成為Login RIB的父級(您應(yīng)該能夠使用上面提供的相同步驟將登錄名連接到root艰管。一些區(qū)別將在RootRouter和RootBuilder中,因?yàn)樗且粋€(gè)頂級RIB蒋川, 沒有父類牲芋。
除了創(chuàng)建RootRouting,我們還需要?jiǎng)?chuàng)建LaunchRouting(為頂級RIB設(shè)計(jì)的特定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
)
}
}
這是一個(gè)非常具體的案例捺球。
RootRouter還將繼承自LaunchRouting而不是ViewableRouter缸浦,后者是特定于啟動的Router協(xié)議:
final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, RootRouting {
override init(interactor: RootInteractable, viewController: RootViewControllable) {
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
}
我們還需要?jiǎng)?chuàng)建一個(gè)AppComponent,它使用具有EmptyDependency的組件氮兵。 該組件將具有我們要使用依賴協(xié)議傳遞的大多數(shù)依賴裂逐。 您可以創(chuàng)建一個(gè)繼承自WebServicing協(xié)議的WebService類,并將其保留為AppComponent中的變量:
final class AppComponent: Component<EmptyDependency>, RootDependency {
}
在AppDelegate中泣栈,我們需要使用此AppComponent創(chuàng)建一個(gè)RootRouter卜高,并在當(dāng)前窗口中對其進(jìn)行啟動:
@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
}
}
在這一點(diǎn)上,我們應(yīng)該能夠編譯并啟動該應(yīng)用程序南片。 如果添加缺少的UI掺涛,則可以看到它的實(shí)際效果。
高級 RIBs
Mock 生成實(shí)體類
我在本文開頭提到疼进,在RIBs中薪缆,我們不使用具體類型,而是在大多數(shù)組件和依賴項(xiàng)中使用協(xié)議伞广。 當(dāng)我們想用單元測試覆蓋我們的代碼時(shí)拣帽,這非常方便疼电。 由于RIBs中的所有業(yè)務(wù)邏輯都存在于Interactor中,因此我們嘗試達(dá)到對 Interactor 和 Router 100%的測試覆蓋率减拭。 協(xié)議允許我們模擬我們使用的大多數(shù)類型澜沟,從而可以在不暴露實(shí)際類型的情況下對其進(jìn)行測試。
但是同時(shí)峡谊,模擬協(xié)議是繁瑣的工作,需要大量樣板代碼刊苍。 幸運(yùn)的是既们,有多種工具可讓我們生成協(xié)議的所有模擬。 其中之一是稱為Mockolo的工具正什。 您可以單擊提供的鏈接并安裝依賴項(xiàng)啥纸,當(dāng)然也可以隨時(shí)使用其他模擬生成工具。 使用Mockolo婴氮,您要做的就是用///@ mockable
注釋標(biāo)記協(xié)議并運(yùn)行模擬生成斯棒。
例如,我們有一個(gè)要在測試中使用的WebServicing協(xié)議主经。 讓我們?yōu)榇朔?wù)生成模擬:
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)
}
}
}
這個(gè) mock類 具有一個(gè)loginCallCount和loginHandler荣暮,我們將使用它們來測試是否調(diào)用了Login方法,以及它是否使用了正確的參數(shù)和結(jié)果罩驻。
我們可以為我們所有的RIB協(xié)議和依賴項(xiàng)生成模擬穗酥,并為廣泛的單元測試范圍打開它。
UnitTests
我將提供一個(gè)示例惠遏,說明如何使用mock生成通過測試覆蓋LoginInteractor砾跃。
讓我們看一下LoginInteractor中的 didTapLogin(:,:)
方法。 這是我們要測試的多種想法:
- 讓presenter顯示 loading狀態(tài)的控件
- 讓webService登錄Web任務(wù)
- 如果登錄任務(wù)成功节吮,則偵聽器應(yīng)調(diào)用dismissLoginFlow方法
- 如果登錄任務(wù)失敗抽高,則演示者應(yīng)調(diào)用showErrorAlert方法
- Web任務(wù)完成時(shí),presenter 隱藏 loading狀態(tài)的控件
這是將所有測試組件連接在一起的LoginInteractorTests的初始設(shè)置(模擬是由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
}
}
讓我們?yōu)閐idTapLogin方法編寫測試透绩。
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 可以用相同的方式進(jìn)行測試雏胃。因?yàn)樵赗IB中,我們將大多數(shù)組件作為協(xié)議志鞍,而不是具體類型瞭亮。 此外,router 和 Interactor 都大多包含實(shí)現(xiàn)其他協(xié)議的方法固棚。 使用mock生成统翩,我們無需編寫任何其他代碼即可使用單元測試覆蓋所有應(yīng)用業(yè)務(wù)邏輯仙蚜。
依賴注入
在示例項(xiàng)目中,我們使用Dependency和Component處理依賴關(guān)系厂汗,并且必須從AppComponent一直傳遞下去委粉。 擁有協(xié)議繼承可以使之清晰明了,井井有條娶桦,但是連接所有依賴項(xiàng)仍然很繁瑣贾节。
我們使用了另一個(gè)開源的Uber工具:Needle Dependency Injection。
在這里衷畦,我不會詳細(xì)解釋Needle栗涂,但是上面的鏈接提供了很好的解釋,并提供了有關(guān)如何集成和使用它的示例祈争。
總結(jié)
在本文中斤程,我介紹了RIB體系結(jié)構(gòu)的要點(diǎn),解釋了一些極端情況菩混,并提供了其大多數(shù)組件的提示和示例忿墅。
對于這個(gè)小項(xiàng)目,RIB看起來像是過分殺傷沮峡,就像我們在示例中使用的那樣疚脐。 但是,如果您了解這些基礎(chǔ)知識邢疙,那么采用這種架構(gòu)不會花費(fèi)太多時(shí)間或精力亮曹。 而且,如果將其與依賴項(xiàng)注入和模擬生成相結(jié)合秘症,將為大多數(shù)應(yīng)用程序用例提供一個(gè)大膽的解決方案照卦。