原標題:基於ReSwift和App Coordinator的iOS架構

本文首發於InfoQ:

http://www.infoq.com/cn/articles/ios-arch-based-on-reswift-and-app-coordinator

iOS架構漫談

當我們在談iOS應用架構時,我們聽到最多的是MVC,MVVM,VIPER這三個Buzz Word,他們的邏輯一脈相承,不斷的從ViewController中把邏輯拆分出去。從蘋果官方推薦的MVC:

基於ReSwift和App Coordinator的iOS架構

(圖片來自:http://t.cn/R4vP8Ko)

隨著系統的複雜,把功能進行細化,把整合View展示數據的邏輯獨立出來形成ViewModel模塊,架構風格就變成了MVVM:

基於ReSwift和App Coordinator的iOS架構

(圖片來自:http://t.cn/R4vP8Ko)

隨著系統的更加複雜,把路由的職責,獲取數據的職責也獨立出去,架構風格就變成了VIPER:

基於ReSwift和App Coordinator的iOS架構

(圖片來自:http://t.cn/R4vP8Ko)

本文則想從另一個角度和大家探討一個新的iOS應用架構方案,架構的本質是管理複雜性,在討論具體的架構方案前,我們首先應該明確一個iOS應用的開發,其複雜性在哪裡?

iOS應用的開發複雜度

對於一個iOS應用來說,其開發的複雜性主要體現在三個方面:

複雜界面設計的實現和樣式管理

iOS App最終呈現給使用者的是一組組的UI界面,而對於一個特定的App來說,其UI的設計元素(如配色,字體大小,間距等)基本上是固定的,另外,組成該App的基礎組件(如Button種類,輸入框種類等)也是有限的。但是如何管理、組合、重用組件則是架構師需要考慮的問題,尤其是一些App在開發過程中可能出現大量的UI樣式重構,更需要清晰的控制住重構的影響範圍。這兒的複雜性本質上是UI組件自身設計實現的複雜性,多UI組件之間的組合方式和UI組件的重用機制。

路由設計

對於一個大型的iOS應用,通常會把其功能按Feature拆分,經過這樣的拆分之後,其可能出現的路由有以下幾種:

基於ReSwift和App Coordinator的iOS架構

APP間路由: 從其它App調起當前App,並進入一個很深層次的頁面(圖示1)。

APP內路由:

  1. 啟動進入App的Home頁面(圖示2)
  2. 從Home頁面到進Feature Flow(圖示3)
  3. Feature內按流程的頁面的路由(圖示4)
  4. 各Feature之間的頁面跳轉(圖示5)
  5. 各Feature共享的單點資訊頁的跳轉(圖示6)

根據Apple官方的MVC架構,這些複雜的各種跳轉邏輯,以及跳轉前的ViewController的準備工作等邏輯纏繞在AppDelegate的初始化,ViewController的UI邏輯中。這兒的複雜性主要是UI和業務之間纏繞不清的相互耦合。

應用狀態管理

一個iOS應用本質上就是一個狀態機,從一個狀態的UI由User Action或者API調用返回的Data Action觸發達到下一個狀態的UI。為了準確的控制應用功能,開發者需要能夠清楚的知道:

  • 應用的當前UI是由哪些狀態決定的?
  • User Action會影響哪些應用狀態?如何影響的?
  • Data Action會影響哪些應用狀態?如何影響的?

在MVC,MVVM,VIPER的架構中,應用的狀態分散在Model或者Entity中,甚至有些狀態直接保存在View Controller中,在跟蹤狀態時經常需要跨越多個Model,很難獲取到一個全貌的應用狀態。另外,對於Action會如何影響應用的狀態跟蹤起來也比較困難,尤其是當一個Action產生的影響路徑不同,或最終可能導致多個Model的狀態發生改變時。這兒的複雜性主要體現在治理分散的狀態,以及管理不統一的狀態改變機制帶來的複雜性。

如何管理這些複雜度

前面明確了iOS應用開發的複雜性所在,那麼從架構層面上應該如何去管理這些複雜性呢?

使用Atomic Design和Component Driven Development管理界面開發的複雜度

UI界面的複雜度本質上是一個點上的複雜度,其複雜性集中在系統的某些小細節處,不會增加系統整體規劃的複雜度,所以控制其複雜度的主要方式是隔離,避免一個UI組件之間的相互交織,變成一個面上的複雜度,導致複雜度不可控。在UI層,最流行的隔離方式就是組件化,在筆者之前的一篇文章《前端組件化方案》中詳細解釋了前端組件化方案的實施細節,這兒就不再贅述。

使用App Coordinator統一管理應用路由

應用的路由主要分為App間路由和App內路由,對它們需要分別處理

App間路由

對於APP之間的路由,主要通過兩種方式實現:

一種是URL Scheme 通過在當前App中配置進行相應的設置,即可從別的APP跳轉到當前APP。進入當前App之後,直接在AppDelegate中的方法:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool

轉換進App內的路由。

另一種是Universal Links,同樣的通過在當前App中進行配置,當使用者點擊URL就會跳轉到當前的App里。進入當前APP之後,直接在AppDelegate中的方法:

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool

中轉進App內路由。

所以App間的路由邏輯相對簡單,就是一個把外部URL映射到內部路由中。這部分只需要增加一個URL Scheme或Universal Link對應到App內路由的處理邏輯即可。

App內路由

對於內部路由,我們可以引入App Coordinator來管理所有路由。App Coordinator是Soroush Khanlou在2015年的NSSpain演講上提出的一個模式,其本質上是Martin Fowler在《Patterns of Enterprise Application Architecture》中描述的Application Controller模式在iOS開發上的應用。其核心理念如下:

  1. 抽象出一個Coordinator對象概念。
  2. 由該Coordinator對象負責ViewController的創建和配置。
  3. 由該Coordinator對象來管理所有的ViewController跳轉。
  4. Coordinator可以派生子Coordinator來管理不同的Feature Flow。

經過這層抽象之後,一個複雜App的路由對應關係就會如下:

基於ReSwift和App Coordinator的iOS架構

從圖中可以看出,應用的UI和業務邏輯被清晰的拆分開,各自有了自己清晰的職責。ViewController的初始化,ViewController之間的鏈接邏輯全部都轉移到App Coordinator的體系中去了,ViewController則徹底變成了一個個獨立的個體,其只負責:

  1. 自己界面內的子UIView組織;
  2. 接收數據並把數據綁定到對應的子UIView展示;
  3. 把界面上的user action轉換為業務上的user intents,然後轉入App Coordinator中進行業務處理。

通過引入AppCoordinator之後,UI和業務邏輯被拆分開,各自處理自己負責的邏輯。在iOS應用中,路由的底層實現還是UINavigationController提供的present,push,pop等函數,在其之上,iOS社區出了各種封裝庫來更好的封裝ViewController之間的跳轉介面,如JLRoutes,routable-ios,MGJRouter等,在這個基礎上我們來進一步思考App Coordinator,其概念核心是把ViewController跳轉和業務邏輯一起抽象為user intents(使用者意圖),對於開發者具體使用什麼樣的方式實現的跳轉邏輯並沒有限制,而路由的實現方式在一個應用中的影響範圍非常廣,切換路由的實現方式基本上就是一次全App的重構(做過React應用的react-router0.13升級的朋友應該深有體會)。所以在App Coordinator的基礎之上,還可以引入Protocol-Oriented Programming的概念,在App Coordinator的具體實現和ViewController之間抽象一層Protocols,把UI和業務邏輯的實現徹底抽離開。經過這層抽象之後,路由關係變化如下:

基於ReSwift和App Coordinator的iOS架構

經過App Coordinator統一處理路由之後,App可以得到如下好處:

  1. ViewController變得非常簡單,成為了一個概念清晰的,獨立的UI組件。這極大的增加了其可復用性。
  2. UI和業務邏輯的抽離也增加了業務程式碼的可復用性,在多屏時代,當你需要為當前應用增加一個iPad版本時,只需要重新做一套iPad UI對接到當前iPhone版的App Coordinator中就完成了。
  3. App Coordinator定義與實現的分離,UI和業務的分離讓應用在做A/B Testing時變得更加容易,可以簡單的使用不同實現的Coordinator,或者不同版本的ViewController即可。

使用 Re S wift管理應用狀態

前面提到引入App Coordinator之後,ViewController剩下的職責之一就是「接收數據並把數據綁定到對應的子UIView展示」,這兒的數據來源就是應用的狀態。它山之石,可以攻玉,不只是iOS應用有複雜狀態管理的問題,在越來越多的邏輯往前端遷移的時代,所有的前端都面臨著類似的問題,而目前Web前端最火的Redux就是為了解決這個問題誕生的狀態管理機制,而ReSwift則把這套機制帶入了iOS的世界。這套機制中主要有一下幾個概念:

  • App State: 在一個時間點上,應用的所有狀態. 只要App State一樣,應用的展現就是一樣的。
  • Store: 保存App State的對象,其還負責發送Action更新App State。
  • Action: 表示一次改變應用狀態的行為,其本身可以攜帶用以改變App State的數據。
  • Reducer: 一個接收當前App State和Action,返回新的App State的小函數。

在這個機制下, 一個App的狀態轉換如下:

  • 啟動初始化App State -> 初始化UI,並把它綁定到對應的App State的屬性上
  • 業務操作 -> 產生Action -> Reducer接收Action和當前App State產生新的AppState -> 更新當前State -> 通知UI AppState有更新 -> UI顯示新的狀態 -> 下一個業務操作……

在這個狀態轉換的過程中,需要注意,業務操作會有兩類:

  • 無非同步調用的操作,如點擊界面把界面數據存儲到App State上;這類操作處理起來非常簡單,按照上面提到的狀態轉換流程走一圈即可。
  • 有非同步調用的操作。如點擊查詢,調用API,數據返回之後再存儲到App State上。這類操作就需要引入一個新的邏輯概念(Action Creators)來處理,通過Action Creators來處理非同步調用並分發新的Action。

整個App的狀態變換過程如下:

基於ReSwift和App Coordinator的iOS架構

無非同步調用操作的狀態流轉

基於ReSwift和App Coordinator的iOS架構

有非同步調用操作的狀態流轉

經過ReSwift統一管理應用狀態之後,App開發可以得到如下好處:

  1. 統一管理應用狀態,包括統一的機制和唯一的狀態容器,這讓應用狀態的改變更容易預測,也更容易調試。
  2. 清晰的邏輯拆分,清晰的程式碼組織方式,讓團隊的協作更加容易。
  3. 函數式的編程方式,每個組件都只做一件小事並且是獨立的小函數,這增加了應用的可測試性。
  4. 單向數據流,數據驅動UI的編程方式。

整理後的iOS架構

經過上面的大篇幅介紹,我們來歸納下結合了App Coordinator和ReSwift的一個iOS App的整體架構圖:

基於ReSwift和App Coordinator的iOS架構

架構實戰

上面已經講解了整體的架構原理,」Talk is cheap」, 接下來就以Raywendlich上面的這個App為例來看看如何實踐這個架構。

基於ReSwift和App Coordinator的iOS架構

(圖片來自:http://t.cn/RCO2Sa0)

第一步:構建UI組件

在構建UI組件時,因為每個組件都是獨立的,所以團隊可以併發的做多個UI頁面,在做頁面時,需要考慮:

  1. 該ViewController包含多少子UIView?子UIView是如何組織在一起的?
  2. 該ViewController需要的數據及該數據的格式?
  3. 該ViewController需要支持哪些業務操作?

以第一個頁面為例:

class SearchSceneViewController: BaseViewController { //定義業務操作的介面 var searchSceneCoordinator:SearchSceneCoordinatorProtocol? //子組件 var searchView:SearchView? //該UI接收的數據結構 private func update(state: AppState) { if let searchCriteria = state.property.searchCriter { searchView?.update(searchCriteria: searchCriteria) } }? //支持的業務操作 func searchByCity(searchCriteria:SearchCriteria) { searchSceneCoordinator?.searchByCity(searchCriteria: searchCriteria) }? func searchByCurrentLocation() { searchSceneCoordinator?.searchByCurrentLocation() } //子組件的組織 override func viewDidLoad() { super.viewDidLoad() searchView = SearchView(frame: self.view.bounds) searchView?.goButtonOnClick = self.searchByCity searchView?.locationButtonOnClick = self.searchByCurrentLocation self.view.addSubview(searchView!) } }

註:子組件支持的操作都以property的形式從外部注入,組件內命名更組件化,不應包含業務含義。

其它的幾個ViewController也依法炮製,完成所有UI組件,這步完成之後,我們就有了App的所有UI組件,以及UI支持的所有操作介面。下一步就是把他們串聯起來,根據業務邏輯完成User Journey。

第二步:構建App Coordinators串聯所有的ViewController

首先,在AppDelegate中加入AppCoordinator,把路由跳轉的邏輯轉移到AppCoordinator中。

var appCoordinator: AppCoordinator! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow() let rootVC = UINavigationController() window?.rootViewController = rootVC appCoordinator = AppCoordinator(rootVC) appCoordinator.start() window?.makeKeyAndVisible() return true }

然後,在AppCoordinator中實現首頁SeachSceneViewController的載入

class AppCoordinator { var rootVC: UINavigationController init(_ rootVC: UINavigationController){ self.rootVC = rootVC } func start() { let searchVC = SearchSceneViewController(); let searchSceneCoordinator = SearchSceneCoordinator(self.rootVC) searchVC.searchSceneCoordinator = searchSceneCoordinator self.rootVC.pushViewController(searchVC, animated: true) } }

在上一步中我們已經為每個ViewController定義好對應的CoordinatorProtocol,也會在這一步中實現

protocol SearchSceneCoordinatorProtocol { func searchByCity(searchCriteria:SearchCriteria) func searchByCurrentLocation() } class SearchSceneCoordinator: AppCoordinator, SearchSceneCoordinatorProtocol { func searchByCity(searchCriteria:SearchCriteria) { self.pushSearchResultViewController() } func searchByCurrentLocation() { self.pushSearchResultViewController() } private func pushSearchResultViewController() { let searchResultVC = SearchResultSceneViewController(); let searchResultCoordinator = SearchResultsSceneCoordinator(self.rootVC) searchResultVC.searchResultCoordinator = searchResultCoordinator self.rootVC.pushViewController(searchResultVC, animated: true) } }

以同樣的方式完成SearchResultSceneCoordinator. 從上面的的程式碼中可以看出,我們跳轉邏輯中只做了兩件事:初始化ViewController和裝配該ViewController對應的Coordinator。這步完成之後,所有UI之間就已經按照業務邏輯串聯起來了。下一步就是根據業務邏輯,讓用App State在UI之間流轉起來。

第三步:引入ReSwift架構構建Redux風格的應用狀態管理機制

首先,跟著ReSwift官方指導選取你喜歡的方式引入ReSwift框架,筆者使用的是Carthage。

定義App State

然後,需要根據業務定義出整個App的State,定義State的方式可以從業務上建模,也可以根據UI需求來建模,筆者偏向於從UI需求建模,這樣的State更容易和UI進行綁定。在本例中主要的State有:

struct AppState: StateType { var property:PropertyState … } struct PropertyState { var searchCriteria:SearchCriteria? var properties:[PropertyDetail]? var selectedProperty:Int = -1 } struct SearchCriteria { let placeName:String? let centerPoint:String? } struct PropertyDetail { var title:String … }

定義好State的模型之後,接著就需要把AppState綁定到Store上,然後直接把Store以全局變數的形式添加到AppDelegate中。

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

把App State綁定到對應的UI上

注入之後,就可以把AppState中的屬性綁定到對應的UI上了,注意,接收數據綁定應該是每個頁面的頂層ViewController,其它的子View都應該只是以property的形式接收ViewController傳遞的值。綁定AppState需要做兩件事:訂閱AppState

override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) mainStore.subscribe(self) { state in state } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) mainStore.unsubscribe(self) }

和實現StoreSubscriber的newState方法

class SearchSceneViewController: StoreSubscriber { …… override func newState(state: AppState) { self.update(state: state) super.newState(state: state) } …… }

經過綁定之後,每一次的AppState修改都會通知到ViewController,ViewController就可以根據AppState中的內容更新自己的UI了。

定義Actions和Reducers實現App State更新機制

綁定好UI和AppState之後,接下來就應該實現改變AppState的機制了,首先需要定義會改變AppState的Action們

struct UpdateSearchCriteria: Action { let searchCriteria:SearchCriteria } ……

然後,在AppCoordinator中根據業務邏輯把對應的Action分發出去, 如果有非同步請求,還需要使用ActionCreator來請求數據,然後再生成Action發送出去

func searchProperties(searchCriteria: SearchCriteria, _ callback:(() -> Void)?) -> ActionCreator { return { state, store in store.dispatch(UpdateSearchCriteria(searchCriteria: searchCriteria)) self.propertyApi.findProperties( searchCriteria: searchCriteria, success: { (response) in store.dispatch(UpdateProperties(response: response)) store.dispatch(EndLoading()) callback?() }, failure: { (error) in store.dispatch(EndLoading()) store.dispatch(SaveErrorMessage(errorMessage: (error?.localizedDeion)!)) } ) return StartLoading() } }

Action分發出去之後,初始化Store時注入的Reducer就會接收到相應的Action,並根據自己的業務邏輯和當前App State的狀態生成一個新的App State

func propertyReducer(_ state: PropertyState?, action: Action) -> PropertyState { var state = state ?? PropertyState() switch action { case let action as UpdateSearchCriteria: state.searchCriteria = action.searchCriteria … default: break } return state }

最終Store以Reducer生成的新App State替換掉老的App State完成了應用狀態的更新。

以上三步就是一個完整的架構實踐步驟,該示例的所有源程式碼可以在筆者的Github上找到。

總結

以解決掉Massive ViewController的iOS應用架構之爭持續多年,筆者也參與了公司內外的多場討論,架構本無好壞,只是各自適應不同的上下文而已。本文中提到的架構方式使用了多種模式,它們各自解決了架構上的一些問題,但並不是一定要捆綁在一起使用,大家完全可以根據需要裁剪出自己需要的模式,希望本文中提到的架構模式能夠給你帶來一些啟迪。

文/ThoughtWorks劉先寧