ReactorKit 是一個(gè)響應(yīng)式、單向 Swift 應(yīng)用框架庶诡。下面來介紹一下 ReactorKit 當(dāng)中的基本概念和使用方法笤闯。
目錄
基本概念
ReactorKit 是 Flux 和 Reactive Programming 的混合體堕阔。用戶的操作和視圖 view 的狀態(tài)通過可被觀察的流傳遞到各層。這些流是單向的:視圖 view 僅能發(fā)出操作(action)流 颗味,反應(yīng)堆僅能發(fā)出狀態(tài)(states)流超陆。
設(shè)計(jì)目標(biāo)
- 可測(cè)性:ReactorKit 的首要目標(biāo)是將業(yè)務(wù)邏輯從視圖 view 上分離。這可以讓代碼方便測(cè)試浦马。一個(gè)反應(yīng)堆不依賴于任何 view时呀。這樣就只需要測(cè)試反應(yīng)堆和 view 數(shù)據(jù)的綁定。測(cè)試方法可點(diǎn)擊查看捐韩。
- 侵入小:ReactorKit 不要求整個(gè)應(yīng)用采用這一種框架退唠。對(duì)于一些特殊的 view鹃锈,可以部分的采用 ReactorKit荤胁。對(duì)于現(xiàn)存的項(xiàng)目,不需要重寫任何東西屎债,就可以直接使用 ReactorKit仅政。
- 更少的鍵入:對(duì)于一些簡(jiǎn)單的功能,ReactorKit 可以減少代碼的復(fù)雜度盆驹。和其他的框架相比圆丹,ReactorKit 需要的代碼更少∏可以從一個(gè)簡(jiǎn)單的功能開始辫封,逐漸擴(kuò)大使用的范圍硝枉。
View
View 用來展示數(shù)據(jù)。 view controller 和 cell 都可以看做一個(gè) view倦微。?view 需要做兩件事:(1)綁定用戶輸入的操作流妻味,(2)將狀態(tài)流綁定到 view 對(duì)應(yīng)的 UI 元素伪煤。view 層沒有業(yè)務(wù)邏輯敷搪,只負(fù)責(zé)綁定操作流和狀態(tài)流审丘。
定義一個(gè) view榕堰,只需要將一個(gè)現(xiàn)存的類符合協(xié)議 View
足删。然后這個(gè)類就自動(dòng)有了一個(gè) reactor
的屬性考赛。view 的這個(gè)屬性通常由外界設(shè)置巢株。
class ProfileViewController: UIViewController, View {
var disposeBag = DisposeBag()
}
profileViewController.reactor = UserViewReactor() // inject reactor
當(dāng)這個(gè) reactor
屬性被設(shè)置(或修改)的時(shí)候廊驼,將自動(dòng)調(diào)用 bind(reactor:)
方法郑临。view 通過實(shí)現(xiàn) bind(reactor:)
來綁定操作流和狀態(tài)流栖博。
func bind(reactor: ProfileViewReactor) {
// action (View -> Reactor)
refreshButton.rx.tap.map { Reactor.Action.refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)
// state (Reactor -> View)
reactor.state.map { $0.isFollowing }
.bind(to: followButton.rx.isSelected)
.disposed(by: self.disposeBag)
}
Storyboard 的支持
如果使用 storyboard 來初始一個(gè) view controller,則需要使用 StoryboardView
協(xié)議牧抵。StoryboardView
協(xié)議和 View
協(xié)議相比笛匙,唯一不同的是 StoryboardView
協(xié)議是在 view 加載結(jié)束之后進(jìn)行綁定的。
let viewController = MyViewController()
viewController.reactor = MyViewReactor() // will not executes `bind(reactor:)` immediately
class MyViewController: UIViewController, StoryboardView {
func bind(reactor: MyViewReactor) {
// this is called after the view is loaded (viewDidLoad)
}
}
Reactor 反應(yīng)堆
反應(yīng)堆 Reactor 層犀变,和 UI 無關(guān)妹孙,它控制著一個(gè) view 的狀態(tài)。reactor 最主要的作用就是將操作流從 view 中分離获枝。每個(gè) view 都有它對(duì)應(yīng)的反應(yīng)堆 reactor蠢正,并且將它所有的邏輯委托給它的反應(yīng)堆 reactor。
定義一個(gè) reactor 時(shí)需要符合 Reactor
協(xié)議省店。這個(gè)協(xié)議要求定義三個(gè)類型: Action
, Mutation
和 State
嚣崭,另外它需要定義一個(gè)名為 initialState
的屬性。
class ProfileViewReactor: Reactor {
// represent user actions
enum Action {
case refreshFollowingStatus(Int)
case follow(Int)
}
// represent state changes
enum Mutation {
case setFollowing(Bool)
}
// represents the current view state
struct State {
var isFollowing: Bool = false
}
let initialState: State = State()
}
Action
表示用戶操作懦傍,State
表示 view 的狀態(tài)雹舀,Mutation
是 Action
和 State
之間的轉(zhuǎn)化橋梁。reactor 將一個(gè) action 流轉(zhuǎn)化到 state 流粗俱,需要兩步:mutate()
和 reduce()
说榆。
mutate()
mutate()
接受一個(gè) Action
,然后產(chǎn)生一個(gè) Observable<Mutation>
寸认。
func mutate(action: Action) -> Observable<Mutation>
所有的副作用應(yīng)該在這個(gè)方法內(nèi)執(zhí)行签财,比如異步操作,或者 API 的調(diào)用偏塞。
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .refreshFollowingStatus(userID): // receive an action
return UserAPI.isFollowing(userID) // create an API stream
.map { (isFollowing: Bool) -> Mutation in
return Mutation.setFollowing(isFollowing) // convert to Mutation stream
}
case let .follow(userID):
return UserAPI.follow()
.map { _ -> Mutation in
return Mutation.setFollowing(true)
}
}
}
reduce()
reduce()
由當(dāng)前的 State
和一個(gè) Mutation
生成一個(gè)新的 State
唱蒸。
func reduce(state: State, mutation: Mutation) -> State
這個(gè)應(yīng)該是一個(gè)簡(jiǎn)單的方法。它應(yīng)該僅僅同步的返回一個(gè)新的 State
灸叼。不要在這個(gè)方法內(nèi)執(zhí)行任何有副作用的操作神汹。
func reduce(state: State, mutation: Mutation) -> State {
var state = state // create a copy of the old state
switch mutation {
case let .setFollowing(isFollowing):
state.isFollowing = isFollowing // manipulate the state, creating a new state
return state // return the new state
}
}
transform()
transform()
用來轉(zhuǎn)化每一種流庆捺。這里包含三種 transforms()
的方法。
func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
通過這些方法可以將流進(jìn)行轉(zhuǎn)化屁魏,或者將流和其他流進(jìn)行合并疼燥。例如:在合并全局事件流時(shí),最好使用 transform(mutation:)
方法蚁堤。點(diǎn)擊查看全局狀態(tài)的更多信息醉者。
另外,也可以通過這些方法進(jìn)行測(cè)試披诗。
func transform(action: Observable<Action>) -> Observable<Action> {
return action.debug("action") // Use RxSwift's debug() operator
}
高級(jí)用法
Global States (全局狀態(tài))
和 Redux 不同, ReactorKit 不需要一個(gè)全局的 app state撬即,這意味著你可以使用任何類型來管理全局 state,例如用 BehaviorSubject
呈队,或者 PublishSubject
剥槐,甚至一個(gè) reactor。ReactorKit 不需要一個(gè)全局狀態(tài)宪摧,所以不管應(yīng)用程序有多特殊粒竖,都可以使用 ReactorKit。
在 Action → Mutation → State 流中几于,沒有使用任何全局的狀態(tài)蕊苗。你可以使用 transform(mutation:)
將一個(gè)全局的 state 轉(zhuǎn)化為 mutation。例如:我們使用一個(gè)全局的 BehaviorSubject
來存儲(chǔ)當(dāng)前授權(quán)的用戶沿彭,當(dāng) currentUser
變化時(shí)朽砰,需要發(fā)出 Mutation.setUser(User?)
,則可以采用下面的方案:
var currentUser: BehaviorSubject<User> // global state
func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}
這樣喉刘,當(dāng) view 每次向 reactor 產(chǎn)生一個(gè) action 或者 currentUser
改變的時(shí)候瞧柔,都會(huì)發(fā)送一個(gè) mutation。
View Communication (View 通信)
多個(gè) view 之間通信時(shí)睦裳,通常會(huì)采用回調(diào)閉包或者代理模式。ReactorKit 建議采用 reactive extensions 來解決廉邑。最常見的 ControlEvent
示例是 UIButton.rx.tap
哥蔚。關(guān)鍵思路就是將自定義的視圖轉(zhuǎn)化為像 UIButton 或者 UILabel 一樣肺素。
假設(shè)我們有一個(gè) ChatViewController
來展示消息猴伶。 ChatViewController
有一個(gè) MessageInputView
筝尾,當(dāng)用戶點(diǎn)擊 MessageInputView
上的發(fā)送按鈕時(shí)筹淫,文字將會(huì)發(fā)送到 ChatViewController
站辉,然后 ChatViewController
綁定到對(duì)應(yīng)的 reactor 的 action。下面是 MessageInputView
的 reactive extensions 的一個(gè)示例:
extension Reactive where Base: MessageInputView {
var sendButtonTap: ControlEvent<String> {
let source = base.sendButton.rx.tap.withLatestFrom(...)
return ControlEvent(events: source)
}
}
這樣就是可以在 ChatViewController
中使用這個(gè)擴(kuò)展损姜。例如:
messageInputView.rx.sendButtonTap
.map(Reactor.Action.send)
.bind(to: reactor.action)
Testing 測(cè)試
ReactorKit 有一個(gè)用于測(cè)試的 built-in 功能饰剥。通過下面的指導(dǎo),你可以很容易測(cè)試 view 和 reactor摧阅。
測(cè)試內(nèi)容
首先汰蓉,你要確定測(cè)試內(nèi)容。有兩個(gè)方面需要測(cè)試棒卷,一個(gè)是 view 或者一個(gè)是 reactor顾孽。
- View
- Action: 能否通過給定的用戶交互發(fā)送給 reactor 對(duì)應(yīng)的 action?
- State: view 能否根據(jù)給定的 state 對(duì)屬性進(jìn)行正確的設(shè)置比规?
- Reactor
- State: state 能否根據(jù) action 進(jìn)行相應(yīng)的修改若厚?
View 測(cè)試
view 可以根據(jù) stub reactor 進(jìn)行測(cè)試。reactor 有一個(gè) stub
的屬性蜒什,它可以打印 actions盹沈,并且強(qiáng)制修改 states。如果啟用了 reactor 的 stub吃谣,mutate()
和 reduce()
將不會(huì)被執(zhí)行乞封。stub 有下面幾個(gè)屬性:
var isEnabled: Bool { get set }
var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions
下面是一些測(cè)試示例:
func testAction_refresh() {
// 1. prepare a stub reactor
let reactor = MyReactor()
reactor.stub.isEnabled = true
// 2. prepare a view with a stub reactor
let view = MyView()
view.reactor = reactor
// 3. send an user interaction programatically
view.refreshControl.sendActions(for: .valueChanged)
// 4. assert actions
XCTAssertEqual(reactor.stub.actions.last, .refresh)
}
func testState_isLoading() {
// 1. prepare a stub reactor
let reactor = MyReactor()
reactor.stub.isEnabled = true
// 2. prepare a view with a stub reactor
let view = MyView()
view.reactor = reactor
// 3. set a stub state
reactor.stub.state.value = MyReactor.State(isLoading: true)
// 4. assert view properties
XCTAssertEqual(view.activityIndicator.isAnimating, true)
}
測(cè)試 Reactor
reactor 可以被單獨(dú)測(cè)試。
func testIsBookmarked() {
let reactor = MyReactor()
reactor.action.onNext(.toggleBookmarked)
XCTAssertEqual(reactor.currentState.isBookmarked, true)
reactor.action.onNext(.toggleBookmarked)
XCTAssertEqual(reactor.currentState.isBookmarked, false)
}
一個(gè) action 有時(shí)會(huì)導(dǎo)致 state 多次改變岗憋。比如肃晚,一個(gè) .refresh
action 首先將 state.isLoading
設(shè)置為 true
,并在刷新結(jié)束后設(shè)置為 false
仔戈。在這種情況下关串,很難用 currentState
測(cè)試 state
的 isLoading
的狀態(tài)更改過程。這時(shí)监徘,你可以使用 RxTest 或 RxExpect晋修。下面是使用 RxExpect 的測(cè)試案例:
func testIsLoading() {
RxExpect("it should change isLoading") { test in
let reactor = test.retain(MyReactor())
test.input(reactor.action, [
next(100, .refresh) // send .refresh at 100 scheduler time
])
test.assert(reactor.state.map { $0.isLoading })
.since(100) // values since 100 scheduler time
.assert([
true, // just after .refresh
false, // after refreshing
])
}
}
Scheduling 調(diào)度
定義 scheduler
屬性來指定發(fā)出和觀察的狀態(tài)流的 scheduler
。注意:這個(gè)隊(duì)列 必須 是一個(gè)串行隊(duì)列凰盔。scheduler
的默認(rèn)值是 CurrentThreadScheduler
墓卦。
final class MyReactor: Reactor {
let scheduler: Scheduler = SerialDispatchQueueScheduler(qos: .default)
func reduce(state: State, mutation: Mutation) -> State {
// executed in a background thread
heavyAndImportantCalculation()
return state
}
}
示例
- Counter: The most simple and basic example of ReactorKit
- GitHub Search: A simple application which provides a GitHub repository search
- RxTodo: iOS Todo Application using ReactorKit
- Cleverbot: iOS Messaging Application using Cleverbot and ReactorKit
- Drrrible: Dribbble for iOS using ReactorKit (App Store)
- Passcode: Passcode for iOS RxSwift, ReactorKit and IGListKit example
- Flickr Search: A simple application which provides a Flickr Photo search with RxSwift and ReactorKit
- ReactorKitExample
依賴
- RxSwift >= 5.0
其他
其他信息可以查看 github