Swift ReactorKit 框架

ReactorKit.png

ReactorKit 是一個(gè)響應(yīng)式、單向 Swift 應(yīng)用框架庶诡。下面來介紹一下 ReactorKit 當(dāng)中的基本概念和使用方法笤闯。

目錄

基本概念

ReactorKit 是 FluxReactive Programming 的混合體堕阔。用戶的操作和視圖 view 的狀態(tài)通過可被觀察的流傳遞到各層。這些流是單向的:視圖 view 僅能發(fā)出操作(action)流 颗味,反應(yīng)堆僅能發(fā)出狀態(tài)(states)流超陆。

image

設(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, MutationState嚣崭,另外它需要定義一個(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)雹舀,MutationActionState 之間的轉(zhuǎn)化橋梁。reactor 將一個(gè) action 流轉(zhuǎn)化到 state 流粗俱,需要兩步:mutate()reduce()说榆。

image

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 一樣肺素。

image

假設(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è)試 stateisLoading 的狀態(tài)更改過程。這時(shí)监徘,你可以使用 RxTestRxExpect晋修。下面是使用 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

依賴

其他

其他信息可以查看 github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市户敬,隨后出現(xiàn)的幾起案子落剪,更是在濱河造成了極大的恐慌睁本,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忠怖,死亡現(xiàn)場(chǎng)離奇詭異呢堰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)凡泣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門枉疼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鞋拟,你說我怎么就攤上這事往衷。” “怎么了严卖?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵席舍,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我哮笆,道長(zhǎng)来颤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任稠肘,我火速辦了婚禮福铅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘项阴。我一直安慰自己滑黔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布环揽。 她就那樣靜靜地躺著略荡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歉胶。 梳的紋絲不亂的頭發(fā)上汛兜,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音通今,去河邊找鬼粥谬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛辫塌,可吹牛的內(nèi)容都是我干的漏策。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼臼氨,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼掺喻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤巢寡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后椰苟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抑月,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年舆蝴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谦絮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洁仗,死狀恐怖层皱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赠潦,我是刑警寧澤叫胖,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站她奥,受9級(jí)特大地震影響瓮增,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜哩俭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一绷跑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凡资,春花似錦砸捏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伞访,卻和暖如春膝藕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咐扭。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工芭挽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蝗肪。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓袜爪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親薛闪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辛馆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前段時(shí)間在RxSwift上做了一些實(shí)踐,Rx確實(shí)是一個(gè)強(qiáng)大的工具,但同時(shí)也是一把雙刃劍昙篙,如果濫用的話反而會(huì)帶來副作...
    L_Zephyr閱讀 4,197評(píng)論 0 15
  • Vuex是什么腊状? Vuex 是一個(gè)專為 Vue.js應(yīng)用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲(chǔ)管理應(yīng)用的所有組件...
    蕭玄辭閱讀 3,118評(píng)論 0 6
  • 在還沒遇到 ReactorKit 這個(gè)框架之前苔可,我使用 RxSwift + MVVM 去構(gòu)建如圖的信息流時(shí)缴挖,確實(shí)為...
    靈度Ling閱讀 7,683評(píng)論 0 41
  • 安裝 npm npm install vuex --save 在一個(gè)模塊化的打包系統(tǒng)中,您必須顯式地通過Vue.u...
    蕭玄辭閱讀 2,938評(píng)論 0 7
  • 這段時(shí)間一直在忙文字工作焚辅,因?yàn)樵趯W(xué)習(xí)部映屋,因?yàn)橛袛?shù)不清的論文,也因?yàn)樽约合胍涗浀纳铧c(diǎn)滴同蜻。有時(shí)候看看別人的生...
    琥珀姑娘閱讀 459評(píng)論 6 12