用 Swift 模仿 Vue + Vuex 進行 iOS 開發(一):ReSwift

ScalaCool發表於2017-12-13

本文由 Yison 發表在 ScalaCool 團隊部落格。

水滴 計劃研發移動端的商家應用,筆者開始了 iOS 端的整體方案設計工作。

由於沒有歷史包袱,且團隊願意嘗試一些不同的方案,經過兩週專注的學習和調研之後,我們並沒有採用主流的 MVVM 架構,而是基於 ReSwift 以及 Swift 這門語言的特性(核心是 extension)構建了一套類似 Vue + Vuex 的方案,筆者打算通過四篇文章來分享下這種思路。

需要注意的是,筆者也是第一次接觸 Swift 和 iOS,某種程度上來說,也是一名 iOS 菜鳥,行文中難免出現不高明之處,還望指正。但與此同時,筆者也有 Scala 和多年的 Web 前端開發經歷,不同的平臺和語言,會有相似的思維和知識結構,所以入門移動端原生應用開發時,也發現很多共同之處。

以下是本系列文章的大綱:

  1. ReSwift
  2. Coordinator
  3. extension
  4. VueLike

架構方式的演變

在介紹 ReSwift 之前,我們先來簡單回顧下 iOS 端(不嚴謹地說,也可以看成是移動端應用開發)的架構演變歷史。

這方面介紹的好文章已經相當的多,重點還是推薦下 @Bohdan Orlov 的 iOS Architecture Patterns,非常的系統和容易理解。

Massive View Controller

在討論架構模式的時候,MVC 是被提及最多的套路之一。眾所周知,Apple 推出的 MVC 跟軟體工程中傳統的 MVC 是不一樣的。

很多人對於經典的 MVC 中的 Model 一直存在誤解,認為其代表的僅僅只是一個實體模型。其實,它準確的概念應該還包含大量的業務邏輯處理,相對的 Controller 只是在 View 和 Model 層建立一個橋樑而已。

注:業界在發展過程中,圍繞 MVC 也延伸討論了很多的問題,典型的如「胖 Model 和瘦 Model」 的問題,甚至於十幾年前,曾經在 JavaEye 上專門針對 Model 的設計有過一次相當激烈的討論,帖子還在。

Apple 的 MVC 採用的是瘦 Model 的設計,ViewController 承載了大量的邏輯處理。之所以這麼設計,也是有原因的。

如果拿 iOS 平臺和瀏覽器進行對比,它們存在大量可類比的部分,但前者有個非常與眾不同的地方,就是 iOS 和 Android 一樣,都存在非常明顯的生命週期,這些生命週期的方法都存在於 ViewController。

所以最初始的 iOS 架構問題顯而易見:過於臃腫的 View Controller 層大大降低了工程的可維護性以及可測試性

這裡推薦下 @Krzysztof Zabłocki 的 Good iOS Application Architecture: MVVM vs. MVC vs. VIPER,他不但講述了對不同架構的理解,也提出了自己對好架構的評判標準。

MVP

解決 Massive View Controller 的一劑良藥來自於 MVP。這種設計思路的核心是提出了一個 Presenter 層,它是連線View層與Model層的橋樑並對業務邏輯進行處理,這個符合了我們理想中的 單一職責原則

MVVM

在筆者看來,MVVM 跟 MVP 其實是十分類似的,這種設計解決了 Massive View Controller 的問題,同時也引入了「雙向資料繫結」,MVVM 也是 Web 前端同學十分熟悉的概念。

可以這麼說,MVVM 應該是當下 iOS 以及 Android 最流行的架構設計。

VIPER

VIPER 是 View + Interactor + Presenter + Entity + Router 的縮寫。對比 Android,這種架構似乎在 iOS 界更流行,但是整體上而言,採用這種架構的設計並不多。理論上,這是一種非常好的架構思想,靈感於所謂的 The Clean Architecture

但更細的模組化設計,也讓 VIPER 被不少人詬病為一種過度工程。對它感興趣的同學,可以看看 objc.io 的 Architecting iOS Apps with VIPER

ReSwift

在水滴內部,我們曾採用過 Angular 1.x 開發業務,所以對於「雙向資料繫結」的概念並不陌生。隨著我們業務的需要,我們過渡到了更加成熟的 Vue 2 + webpack 來組織 Web 前端的開發。在體驗過不同的資料流方案之後,就偏好而言,我們還是更加喜愛「單向資料流」的套路,緣自於後者設計更簡單,更有利於測試。

所以,在學習了 MVVM 這個成熟的解決方案之後,筆者也開始尋求 iOS 的單向資料流解決方案,後面發現了ReSwift,在經過兩週的體驗和測試,我們發現這或許是更加符合團隊審美偏好的一種架構設計。

Redux

要了解「單向資料流」其實只要學習 Redux 就行了。2014年 Facebook 提出了 Flux 架構的概念,2015年,Redux 出現,將 Flux 與函數語言程式設計結合一起,很短時間內就成為了最熱門的 Web 前端架構。

核心設計

基於經典的 Redux 模型,ReSwift 也奉行以下設計:

  • The Store:以單一資料結構管理整個 app 的狀態,狀態只能通過 dispatching Actions 來進行修改。一旦 store 中的狀態改變了,它就會通知給所有的 observers 。

  • Actions:通過陳述的形式來描述一次狀態變更,它不包含任何程式碼,儲存在 store,被轉發給 reducers。reducers 會接收這些 actions 然後進行相應的狀態邏輯變更。

  • Reducers:基於當前的 action 和 app 狀態,通過純函式來返回一個新的 app 狀態。

combineReducers

筆者發現在當前的 ReSwift 版本中,並沒有提供 Redux 中相應的 combineReducers 實現。猜想這個其實跟 Swift 與 JavaScript 之間的差異導致,與後者這門動態語言不通,前者存在靜態的型別。但這個問題可以通過其它辦法來解決。

牛刀小試

現在我們就來看看如何基於 ReSwift 建立一個 iOS 工程。

首先是專案結構設計,假設這是一個多功能的業務需求,看 ReSwift 是否可以組織一個相對複雜的專案。

專案結構

  • App
    • AppReducer.swift
    • AppState.swift
  • Modules
    • Module1
      • Actions
      • Reducers
      • State
    • Module2
      • ……
  • Views
  • AppDelegte.swift
  • ……

AppDelegate.swift

import UIKit
import ReSwift

let mainStore = Store<AppState>(
    reducer: appReducer,
    state: nil
)

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    ……
}

複製程式碼

App/AppState.swift

import ReSwift

struct AppState: StateType {
    var module1State: Module1State
    var module2State: Module2State
}
複製程式碼

App/AppReducer.swift

import ReSwift
import ReSwiftRouter

func appReducer(action: Action, state: AppState?) -> AppState {
    return AppState(
        module1State: module1Reducer(action: action, module1State: state?.module1State),
        module2State: module2Reducer(action: action, module2State: state?.module2State)
    )
}

複製程式碼

Modules/Module1/State/Module1State.swift

import ReSwift

struct Module1State {
  ……
}
複製程式碼

Modules/Module1/Reducers/Module1Reducer.swift

import ReSwift

func module1Reducer(action: Action, module1State: Module1State?) -> Module1State {
    return doSomething(module1State) ?? Module1State()
}
複製程式碼

Modules/Module1/Actions/Module1Action.swift

import ReSwift

struct Module1Action {
    func action1(params: Int) -> Action {
        return Action1(params: params)
    }
}

extension Module1Action {
    struct Action1: Action {
        let params: Int
    }
}
複製程式碼

就這樣,我們完成了 Redux 相關的結構設計,至於 Redux 跟 ViewController 層如何結合,打交道。我們將在下一篇關於 Coordinator 的文章中進一步介紹。

用 Swift 模仿 Vue + Vuex 進行 iOS 開發(一):ReSwift

相關文章