還記得在 2017 的時候,@khanlou 曾經給過一個 talk,是關於 Coordinator 的 talk。當時的我也還是個小菜雞,只要看到各種關於 architecture 的東西都會莫名興奮。
這個 talk 想要解決的是當 app 越來越大、越來越複雜的時候的一個處理方式。當時 Coordinator 被提出的時候也並沒有太多人去實作出來,比較偏向是一個概念,以至於小菜雞如果就會開始 google 看看有沒有 library 可以幫我們把 coordinator 實作完,可以讓我們直接套到我們 app 上的 solution 呢?想當然是沒有。於是把 app 改成使用 coordinator 架構這件事就這樣被擺在一旁。
接著過了快一年,剛好公司有一個專案需要從頭開始,這個時候是最適合導入各種你想要、你喜歡的架構的時候。於是選定好 MVVM + Coordinator 當成 project base architecture,開始了 Coordinator 這個不歸路了。
為什麼會想要嘗試 FlowController
這邊直接舉一些當時使用 Coordinator 所遇到的問題
- 當我們在使用 navigation controller,遇到要監控 back button click 事件
- 要處理 UIWindow
- 為什麼需要
.start()
方法 - 為什麼要把 coordinator 記在一個 array 裡面
- 一個 Coordinator 只能對應一個 View 嗎?
在嘗試了 Coordinator pattern 一年,也就是一個專案從 0 到 1 之後,這些問題是我所遇到,想要嘗試解決、或者找到更好解法的一些問題點。當時剛好看到 Khoa 在 github 上提了這個 issue,讓我想嘗試使用 FlowController,也剛好又有一個新專案可以讓我嘗試 FlowController + MVVM + Combine + Input & Output data binding pattern(是不是腦洞大開啦)。
不過這裡我們還是圍繞在 FlowController 就好,因為你會漸漸發現
不管你用的 architecture 再怎麼華麗花俏,最終還是得回歸 SOLID 原則,好讀好維護,寫別人讀得懂的程式碼才是最重要的。
Architecture 終究只是 architectural pattern
。
FlowController?
我們可以先看看 FlowController 大致上長怎樣:
跟 Coordinator 不一樣的點我們可以在這裡就看出來,我們並不需要將 window
丟給 AppCoordinator 去控制,只要將 AppFlowController 加到 window 上,因為他本來就是 UIViewController
based,所以保有 UIResponder chain 特性,可以直接顯示 AppFlowController 上第一個 view。
如此一來,我們就不需要將 window 在 coordinator 間互相傳遞,而是只要在 AppFlowController 中控制好我們的 child vc 即可。 (問題點 2 解決)
再來來看 第 1 個問題點
,以往我們在 Coordinator 想要偵測 back button click,我們第一個想到的是把 coordinator 中的 navigationController.delegate
丟給 coordinator 處理,想要這樣的話,我們要讓 coordinator conform UINavigationControllerDelegate
才行,從這裡開始就違背 coordinator 就是單純一個 coordinating object 的初衷了,於是我們可能會將 navigation controller 另外包在一個 navigator or router 中去特殊處理。
但可以回過來看 FlowController:
由於 FlowController 本身是 UIViewController based,讓 FlowController 去實作 UINavigationControllerDelegate
並不會有什麼特別的問題。
接著看 第 3 個問題點
,我們很常在各個文章中看到每個 coordinator 被加入到 coordinator array 中之後還要呼叫 coordinator.start()
,且在
定義一個 start()
method,以至於每個 coordinator 好像必須要 start()
才會動作,其實是不必要的。
Coordinator 最需要的只有 children: [CoordinatorType]
需要被記起來而已。
會說 start()
不是必須是因為如果你的 app 有三四種不同的入口,那你把 start()
直接寫死在 coordinator 之中不就把自己給綁死了嗎?
使用 FlowController 可以少 call start()
的一個原因是因為他是 UIViewController
based,在你從 a flow present 到 b flow 之時,他就是單純的 view 之間的 present & dismiss,走的就是我們熟悉的轉場。
問題點 4
是為什麼我們要把 children coordinators 存起來呢?
是因為我們需要去觀察每個 coordinator 之間的前後端係,就如同 navigation controller 可以知道目前的 view stack,然後去決定我們是否要將前幾個 view 刪除,或者當 c 前面是 b 而不是 a 時,我們會將 c 的資料提前更新到 b 畫面上的時候,我們都需要知道 view 與 view 之間的前後關係,coordinator 間也是一樣的。
但 coordinator 會盡量不去碰 view 上所發生的事,coordinator 需要做的事情是專注管理 coordinator 之間的關係,所以透過 view 反推我們應該去操作哪一個 coordinator 並不明智。
但如果是由 FlowController 去管理,因為 view stack 本身就是一個樹狀結構,我們可以從 FlowController 去看我們前後有哪一些 view。
比如在 FlowController 中如果我們持有 navigation controller,我們可以去看 navigation stack 目前有哪些 view。
也可以去看 presenting or presented view 有哪些,會比 coordinator 少一點轉換。
問題點 5
一個 Coordinator 只能對應一個 View 嗎?
我們也會在很多文章中看到一個 Coordinator 只會去對應一個 View,這就會讓我們產生一個錯覺,是不是代表如果流程很長,AppCoordinator 中就會有很多 children coordinator?
答案是否定的,這邊也是我認為 FlowController 的命名其實比 Coordinator 更為精確的一個點。
比如我們有一個大的入口叫 AppFlowController,他的職責就是去根據 app 狀態,去執行各種不同的 flow 的管理員。
假設我們現在要登入,可能就會經過 Intro > Policy > Sign up > Enter Password > Setup Face ID login 等等流程,其實我們可以把這些細碎的畫面包在一個 LoginFlowController 之中。
如此一來我們可以延伸到,HomeFlowController, ProductFlowController, SettingFlowController 等等,然後將這些 sub-flow controller 包進 AppFlowController。這邊我們處理的方式就是在歸納分類,把適合被歸納在同一類的份在一個 domain 之中,如果該 domain 東西變肥變大了,就再去找出可以分類的部分,再切開。
同理 Coordinator 也可以做到一樣的事情。
可以看到這張圖,可以想見 A, B, C, D 其實都可以是 sub-flow controller,甚至 A 底下也可以再延伸更多的 sub-flow controller,切分出每個 sub-flow 的 domain
總結
FlowController 並不是要取代 Coordinator 的 pattern,他只是解決了 Coordinator 使用上的一些小小的不方便的延伸性 pattern。
我們可以發現其實 FlowController 跟 Coordinator 其實是非常類似的架構,我們想要做的事情都是把有可能由 view 去 push 到下一個 view 可能會造成的相依性,丟給 FlowController 跟 Coordinator 去統一處理,view 只管去告訴 FlowController 跟 Coordinator 目前的狀況如何,再交由 FlowController 去看要 push 到下一個 view 還是要做什麼處理。
套用了 FlowController 跟 Coordinator 之後我們可以預見 view 跟 view model 之間會有強耦合性,但是 view 跟 view 之間並不會有耦合性,因為 view 之間的轉場我們交由 FlowController 去處理了。如果我們在其他的 FlowController 想要使用到某個特定的 view 也是可以很容易的被 reuse 的。
Murmur
這邊給剛接觸 iOS 開發或者正準備學 MVVM 或者 MVVM 寫完覺得哇現在自己超強的人一些檢視目前成果的一些問題:
- 如果不用 MVVM,而是維持在 MVC 的話,我能不能把 code 一樣寫得很好讀很好維護?
- 有沒有除了 MVVM 以外的架構?
- 可以試著了解一下 MVP, VIP, VIPER, TEA, Redux 等等架構,跟他們試著解決什麼問題?
- 為什麼 Uber 基於 VIPER 去延伸了 UIBs 架構?
- 我今天能讓 MVVM, MVP, VIPER 共存於一個專案中嗎?
- 有沒有想過從 MVC, MVVM 到 MVVM-C,其實我們一直圍繞在 SOLID 原則上在解決我們遇到的問題,只是我們不自知?
- 有沒有想過我們所說的架構架構,更多的只是在講
architectural pattern
? - 如果我們今天不寫 iOS 了,我們可以將這些好東西帶往不同領域,並且用上嗎?
在找尋一個適合團隊的架構,更多時候是一個循序漸進、嘗試錯誤、溝通、慢慢累積的過程。
從來沒有某一個架構,可以直接被放到團隊中,然後馬上變得哇,超狂,馬上解決團隊中所有問題。
這邊可以跟各位分享一個案例:
比如在 VIPER 中要觀察 keyboard show/hide,我應該要放在 View, Interactor 還是 Presenter 處理?
如果放在 view,那我如果有一些高度計算可能需要交給 interactor 或者 Presenter 計算怎麼辦?
如果放在 interactor or Presenter,那我需要 import UIKit 會破壞原則怎麼辦?
同時也可以延伸出一個問題,我不想在 view model import UIKit,但我的 view model 需要用到 UIImage,我是不是該轉成 data 存在 vm 中呢(然後你每次要用都要轉換,幹嘛這麼麻煩)?
這些都是開放式問答,需要跟團隊內的人溝通好,你想在 vm 中使用 UIImage,為什麼不可以?不要被架構或者是一些無所謂的堅持限制住了。
以上☝️!
我覺得我好囉唆 🤪
參考資料
其他更細節的東西可以看一下下面文章,裡面有更詳細的比較。