ReactorKit 是一個響應式、單向 Swift 應用框架。下面來介紹一下 ReactorKit 當中的基本概念和使用方法。react
ReactorKit 是 Flux 和 Reactive Programming 的混合體。用戶的操做和視圖 view 的狀態經過可被觀察的流傳遞到各層。這些流是單向的:視圖 view 僅能發出操做(action)流 ,反應堆僅能發出狀態(states)流。git
View 用來展現數據。 view controller 和 cell 均可以看作一個 view。view 須要作兩件事:(1)綁定用戶輸入的操做流,(2)將狀態流綁定到 view 對應的 UI 元素。view 層沒有業務邏輯,只負責綁定操做流和狀態流。github
定義一個 view,只須要將一個現存的類符合協議 View
。而後這個類就自動有了一個 reactor
的屬性。view 的這個屬性一般由外界設置。swift
class ProfileViewController: UIViewController, View {
var disposeBag = DisposeBag()
}
profileViewController.reactor = UserViewReactor() // inject reactor
複製代碼
當這個 reactor
屬性被設置(或修改)的時候,將自動調用 bind(reactor:)
方法。view 經過實現 bind(reactor:)
來綁定操做流和狀態流。閉包
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 來初始一個 view controller,則須要使用 StoryboardView
協議。StoryboardView
協議和 View
協議相比,惟一不一樣的是 StoryboardView
協議是在 view 加載結束以後進行綁定的。app
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 層,和 UI 無關,它控制着一個 view 的狀態。reactor 最主要的做用就是將操做流從 view 中分離。每一個 view 都有它對應的反應堆 reactor,而且將它全部的邏輯委託給它的反應堆 reactor。框架
定義一個 reactor 時須要符合 Reactor
協議。這個協議要求定義三個類型: Action
, Mutation
和 State
,另外它須要定義一個名爲 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 的狀態,Mutation
是 Action
和 State
之間的轉化橋樑。reactor 將一個 action 流轉化到 state 流,須要兩步:mutate()
和 reduce()
。ide
mutate()
mutate()
接受一個 Action
,而後產生一個 Observable<Mutation>
。測試
func mutate(action: Action) -> Observable<Mutation>
複製代碼
全部的反作用應該在這個方法內執行,好比異步操做,或者 API 的調用。
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()
由當前的 State
和一個 Mutation
生成一個新的 State
。
func reduce(state: State, mutation: Mutation) -> State
複製代碼
這個應該是一個簡單的方法。它應該僅僅同步的返回一個新的 State
。不要在這個方法內執行任何有反作用的操做。
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()
用來轉化每一種流。這裏包含三種 transforms()
的方法。
func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>
複製代碼
經過這些方法能夠將流進行轉化,或者將流和其餘流進行合併。例如:在合併全局事件流時,最好使用 transform(mutation:)
方法。點擊查看全局狀態的更多信息。
另外,也能夠經過這些方法進行測試。
func transform(action: Observable<Action>) -> Observable<Action> {
return action.debug("action") // Use RxSwift's debug() operator
}
複製代碼
和 Redux 不一樣, ReactorKit 不須要一個全局的 app state,這意味着你能夠使用任何類型來管理全局 state,例如用 BehaviorSubject
,或者 PublishSubject
,甚至一個 reactor。ReactorKit 不須要一個全局狀態,因此無論應用程序有多特殊,均可以使用 ReactorKit。
在 Action → Mutation → State 流中,沒有使用任何全局的狀態。你能夠使用 transform(mutation:)
將一個全局的 state 轉化爲 mutation。例如:咱們使用一個全局的 BehaviorSubject
來存儲當前受權的用戶,當 currentUser
變化時,須要發出 Mutation.setUser(User?)
,則能夠採用下面的方案:
var currentUser: BehaviorSubject<User> // global state
func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}
複製代碼
這樣,當 view 每次向 reactor 產生一個 action 或者 currentUser
改變的時候,都會發送一個 mutation。
多個 view 之間通訊時,一般會採用回調閉包或者代理模式。ReactorKit 建議採用 reactive extensions 來解決。最多見的 ControlEvent
示例是 UIButton.rx.tap
。關鍵思路就是將自定義的視圖轉化爲像 UIButton 或者 UILabel 同樣。
假設咱們有一個 ChatViewController
來展現消息。 ChatViewController
有一個 MessageInputView
,當用戶點擊 MessageInputView
上的發送按鈕時,文字將會發送到 ChatViewController
,而後 ChatViewController
綁定到對應的 reactor 的 action。下面是 MessageInputView
的 reactive extensions 的一個示例:
extension Reactive where Base: MessageInputView {
var sendButtonTap: ControlEvent<String> {
let source = base.sendButton.rx.tap.withLatestFrom(...)
return ControlEvent(events: source)
}
}
複製代碼
這樣就是能夠在 ChatViewController
中使用這個擴展。例如:
messageInputView.rx.sendButtonTap
.map(Reactor.Action.send)
.bind(to: reactor.action)
複製代碼
ReactorKit 有一個用於測試的 built-in 功能。經過下面的指導,你能夠很容易測試 view 和 reactor。
首先,你要肯定測試內容。有兩個方面須要測試,一個是 view 或者一個是 reactor。
view 能夠根據 stub reactor 進行測試。reactor 有一個 stub
的屬性,它能夠打印 actions,而且強制修改 states。若是啓用了 reactor 的 stub,mutate()
和 reduce()
將不會被執行。stub 有下面幾個屬性:
var isEnabled: Bool { get set }
var state: StateRelay<Reactor.State> { get }
var action: ActionSubject<Reactor.Action> { get }
var actions: [Reactor.Action] { get } // recorded actions
複製代碼
下面是一些測試示例:
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)
}
複製代碼
reactor 能夠被單獨測試。
func testIsBookmarked() {
let reactor = MyReactor()
reactor.action.onNext(.toggleBookmarked)
XCTAssertEqual(reactor.currentState.isBookmarked, true)
reactor.action.onNext(.toggleBookmarked)
XCTAssertEqual(reactor.currentState.isBookmarked, false)
}
複製代碼
一個 action 有時會致使 state 屢次改變。好比,一個 .refresh
action 首先將 state.isLoading
設置爲 true
,並在刷新結束後設置爲 false
。在這種狀況下,很難用 currentState
測試 state
的 isLoading
的狀態更改過程。這時,你能夠使用 RxTest 或 RxExpect。下面是使用 RxExpect 的測試案例:
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
])
}
}
複製代碼
定義 scheduler
屬性來指定發出和觀察的狀態流的 scheduler
。注意:這個隊列 必須 是一個串行隊列。scheduler
的默認值是 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
}
}
複製代碼
其餘信息能夠查看 github