iOS 工程

[iOS 架構 #9] ReactorKit 總結:單向架構的標準

ReactorKit 是讓 UIKit 與 RxSwift 組合採用單向資料流作為標準的架構。本文整理 View 與 Reactor 的兩種角色、需要 Mutation 中間階段的原因,以及它與 TCA 的差異。

閱讀 5 分鐘
[iOS 架構 #9] ReactorKit 總結:單向架構的標準 封面圖

在 TCA(The Composable Architecture)篇中,我們介紹了單向資料流。其實,韓國 iOS 圈早就有一個更早將單向架構建立為標準的框架,那就是 ReactorKit。

這個由全秀烈(Suyeol Jeon)於 2017 年公開的框架,在 StyleShare 與 Kakao 系列服務導入後,成為 UIKit + RxSwift 組合的事實標準架構。甚至曾有一段時間,徵才公告經常寫著「具 ReactorKit 經驗者優先」。

今天將整理 ReactorKit 的結構、它與 TCA 的差異,以及它目前的定位。


View 與 Reactor,角色只有兩個

ReactorKit 的結構很簡單。每個畫面都分成 View 與 Reactor。

  • View:將使用者輸入流向Action,並訂閱State來繪製畫面。不包含邏輯。
  • Reactor:接收 Action 並產生 State 的邏輯單元。完全不了解 UI。

重點在於,Reactor 內部的資料流被固定為單一方向。

Action → mutate() → Mutation → reduce() → State → View

按鈕點擊以Action.refresh進入時,mutate()會處理網路請求等副作用,並發出Mutation.setItems([...])reduce()接收這個 Mutation 後計算新的 State。View 訂閱 State,接著更新畫面。

為什麼需要 Mutation 這個中間階段

如果你熟悉 MVVM(Model-View-ViewModel),可能會想:「直接從 Action 修改 State 不就好了嗎?」之所以加入 Mutation,是為了隔離非同步處理

mutate()是唯一允許執行非同步工作的地方。相對地,reduce()是純函式,因此輸入相同的 State 與 Mutation 時,永遠會得到相同結果。副作用被固定在單一位置,因此追蹤狀態為何改變時,也只需查看一個地方。

final class SearchReactor: Reactor {
    enum Action { case updateQuery(String) }
    enum Mutation { case setResults([Repo]) }

    struct State { var results: [Repo] = [] }

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .updateQuery(let query):
            return service.search(query)
                .map { Mutation.setResults($0) }
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setResults(let repos): newState.results = repos
        }
        return newState
    }
}

Reactor 是不了解 UI 的純邏輯,因此測試也很簡單。傳入 Action,再確認 State 即可。

ReactorKit Action mutate Mutation reduce State 單向流程圖
非同步處理隔離在 mutate(),純計算隔離在 reduce()

與 TCA 有什麼不同

兩者都是受到 Redux 影響的單向架構,但套用單位不同。

ReactorKit TCA
基礎 RxSwift Combine·Swift Concurrency
套用單位 每個畫面(View)一個 Reactor 將功能單位的 Reducer 合成樹狀結構
導入方式 可逐一畫面漸進式導入 將整個 App 建構為單一體系
主要場景 UIKit SwiftUI

ReactorKit 的實用性來自導入方式。在既有 MVC(Model-View-Controller)專案中,只要挑選一個畫面並加上 Reactor 就能運作,不需要重寫整個 App。相較之下,TCA 會連功能之間的狀態合成一起管理,但必須採用整套體系。

另外也曾有將 Redux 直接移植到 iOS 的 ReSwift 函式庫,但 App 全域單一儲存庫的方式難以與 UIKit 配合,因此沒有像 ReactorKit 一樣普及。

現在的 ReactorKit

坦白說,在新專案中選擇 ReactorKit 的情況正在減少,因為它的弱點已變得明顯。

第一,它與 RxSwift 強烈耦合。隨著 Apple 生態系轉向 Combine 與 Swift Concurrency,RxSwift 本身的相依性也成了負擔。

第二,它不適合 SwiftUI。View 協定與繫結方式是以 UIKit 的命令式更新為前提設計的。

不過,ReactorKit 留下的影響十分明確。這個框架最早將「狀態只會往一個方向流動」、「將副作用隔離在單一位置」的觀念帶給韓國 iOS 開發者。現在學習 TCA 的人,應該不會對 Action・Mutation・State 結構感到陌生;其中相當一部分觀念是在 ReactorKit 時代形成的。

如果你正在維護既有的 UIKit + RxSwift 程式碼庫,ReactorKit 仍是經過驗證的選擇。由於它以畫面為單位掛載與移除,之後也能逐畫面移除並進行遷移,相對容易。

從 ReactorKit 將單向流程接力棒傳給 TCA 的接力插圖
單向的觀念由 ReactorKit 率先建立,再由 TCA 接續傳承

總結

  • ReactorKit 是以 RxSwift 為基礎的架構,將單一畫面拆分為 View 與 Reactor,並強制採用 Action → Mutation → State 的單向流程。
  • 非同步處理隔離在mutate(),純狀態計算隔離在reduce(),因此容易追蹤與測試。
  • 它與 TCA 的概念相同,但可依畫面漸進式導入的實用路線是其差異所在。
  • 由於與 RxSwift 耦合且不相容於 SwiftUI,新專案導入已減少,但在 UIKit 舊有程式碼中仍然有效。

下一篇將介紹 Uber 的 RIBs,探討如何以商業邏輯而非畫面單位拆分 App。我們會深入分析 VIPER 篇中只用一段提到的那個架構。


參考資料

延伸閱讀