你是否也曾因為一在發生的 UI 錯誤煩惱? 當 QA 又
回報某個頁面錯誤,[Bug] 精選頁面 Load more 未顯示 Loading indicator,為什麼要說又呢?
如果你也有這樣的症頭,請繼續看下去。

雖然我們依循 Model-View-Controller(MVC) 的架構開發,但對於複雜的 UI 狀態改變還是會出錯。
哪裡處了問題?
Class KKFeaturedViewController: UIViewController {
var cards: [Card]
var isLoading: Bool
var isReloading: Bool
var hasMore: Bool
var loaded: Bool
// ...
}
大家都寫得差不多,建立多個 flag 來判斷不同的狀態,這樣的程式碼哪裡有問題? 讓我們回到 QA 回報的問題 [Bug] 精選頁面 Load more 未顯示 Loading indicator ,可以從上面的變數中推測當 Load more 時,會是什麼狀態?
- cards.count > 0
- isLoading == true
- isReloading == false
- hasMore == false || true
- loaded == true
由下列程式碼中很清楚的光是 cards.count
跟 hasMore
就定義了四種狀態。更不用說在此架構下若要滴水不漏,你需要處理 2 的 4 次方,也就是 16 種狀態。
Class KKFeaturedViewController: UIViewController {
// ...
func fetchCardAPI() {
// ...
isLoading = true
if hasMore {
if cards.count == 0 {
// First load without content (reload)
}
else {
// Loading more with content (load more)
}
}
else {
if cards.count == 0 {
// Empty content
}
else {
// Content complete
}
}
}
}
不存在狀態 if (loaded == false && cards.count > 0) { // 不該存在的狀態 }
16 個狀態組合中若不小心出現了不該出現的狀態,Bug 就產生了。同時而且這樣的程式碼不容易分辨哪些狀態有處理過,哪些沒有,像是複雜的電路板到處接來接去,出錯時不容易找到問題出在哪,當然也就不容易修正。

我想有限狀態機 可以幫我解決這個問題。有限狀態機的概念是 在有線個數的狀態裡,狀態之間轉移的數學模型
。 主要的元素有 狀態
, 動作(事件)
來延伸出 狀態A
+ 事件
=> 狀態B
。最常見的應用是交通燈(紅綠燈)
,也就是 綠燈時
+ 60秒
=> 黃燈
以此類推。
回過頭來整理真正會出現的狀態,原本 16 種狀態只有成 8 種會出現,一半以上的狀態都可以忽略 (難怪這麼多 Bug )
參考How to fix a bad user interface一文中並實作其 UI Stack 的概念。

狀態實作
在我們情境下需要考慮到較多的狀態,所以就從原本的 5 種延伸成下列 8 種(請你視使用情境而自行調整成需要的狀態)。
將 ViewController 的權責從 處理狀態轉換 + 過濾不必要的狀態 + 各個子UI管理
被簡化成 根據狀態顯示UI
,並且集中管理。
enum UIStack {
// Blank State
case initial // 初始狀態尚未開始載入資料 // Loading State
case initialLoading // 初始載入,可與 Reload 共用
case partialWithLoading // 載入部分資料,但又觸發載入下一頁 // Partial State
case partial //載入部分資料
case partialWithError //載入部分資料,但載入新資料過程有誤 // Error State
case error // 錯誤 // Ideal State
case perfect // 資料完整載入
case empty // 打過 API 但是沒有資料 (empty inbox 的概念)
}
Class ListViewController: UIViewController {
internal var state: UIStack = .initial {
didSet {
// * switch cases ... * //
}
}
}
總結
把設定狀態
跟 顯示UI
的邏輯分離後會讓事情簡單很多,防止自己粗心大意。
開發加速
開發過程中可隨時模擬狀態,就連部分載入但有錯誤的情境都可輕易重現。 舉例來說,想要重現 contentEmpty
這個狀態,在以前我可能要去修改 API Clint 去偽裝這隻 API 回應給我 0 個資料,而現在只要直接改 state
即可。
容易除錯
當 UI 出錯時就可以分成兩種錯誤,一種是狀態錯了,另一種是對應的 UI 錯了。
程式碼集中
跟狀態相關的程式碼會集中在一個地方,統一實作。
防呆
狀態一但被定義,Swift 的 Switch case 會強迫你要實作所有 cases,不讓我們有偷懶的機會。