最近被一個 SwiftUI 架構徹底改變了我的三觀
今天要來聊聊一個最近搞得我有點...啊,應該說是醍醐灌頂的東西:The Composable Architecture,大家簡稱 TCA。老實說,我寫 App 也好一陣子了,大概一年前吧,才開始真正嚴肅看待「架構」這回事。不然以前就是...你知道的,`@State`、`@ObservedObject` 隨便用,能動就好。😅
但專案一長大,那個混亂的程度真的是災難。code 散得到處都是,改一個東西怕壞掉十個地方,光是找一個 bug 就要追半天,看到最後都懷疑人生。直到我今年初撞牆期,遇到了 TCA,才發現原來 SwiftUI 的世界可以這麼有秩序。
網路上很多 TCA 教學寫得超詳細,但我自己是覺得,很多都太理論了。所以這篇我想用更白話、更像我自言自語的方式,分享我從原本混亂的開發模式,跳到 TCA 後的那些「啊哈!」瞬間,還有一些我希望自己早點知道的鳥事。
先說結論:TCA 到底在幹嘛?
簡單講,TCA 就是一個由國外大神 Brandon Williams 跟 Stephen Celis(就是做 Point-Free 影片教學的那兩位)打造的函式庫兼設計模式。它的目的很純粹:幫你在複雜的 SwiftUI App 中,用一套超級一致、可預測、而且好測試的方式管理狀態、處理各種副作用(像是打 API)。
我自己是覺得,TCA 最強的地方在於它的「強制性」。它會逼你把所有跟一個功能相關的 State、Action 全部定義在同一個地方。所有的狀態更新,都必須透過一個固定的流程走。一開始你可能會覺得「哇靠,好煩,超綁手綁腳的」,但這正是它的威力所在。當你的 App 長到幾十個頁面時,你會回來感謝這個「煩」。因為你永遠都知道,一個狀態的改變是從哪裡發起的、為什麼會發生。除錯跟交接給新同事,都變得簡單太多了。
所以它到底長怎樣?直接看 Code 比較快
講一堆理論不如直接上 Code,真的沒那麼可怕。我們來看一個最簡單的計數器例子,這幾乎是所有教學的 Hello World。
首先,你會定義一個 feature 的所有東西:
import ComposableArchitecture
import SwiftUI
// 這就是你的「功能模組」
@Reducer
struct CounterFeature {
// 1. State: 這功能需要的所有資料
struct State: Equatable {
var count = 0
}
// 2. Action: 所有可能發生的事情
enum Action: Equatable {
case increment
case decrement
}
// 3. Reducer: 大腦!決定收到 Action 後 State 該怎麼變
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1
return .none // .none 代表「狀態改完了,沒其他事要做了」
case .decrement:
state.count -= 1
return .none
}
}
}
}
然後在你的 SwiftUI View 裡面這樣用它:
struct CounterView: View {
// Store: 負責掌管一切的引擎
let store: StoreOf<CounterFeature>
var body: some View {
// WithViewStore 是個好幫手,讓 View 只在需要的時候更新
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 20) {
Text("Count: \(viewStore.count)")
.font(.largeTitle)
HStack(spacing: 15) {
Button("−") { viewStore.send(.decrement) }
Button("+") { viewStore.send(.increment) }
}
}
.buttonStyle(.borderedProminent)
}
}
}
看到了嗎?View 的責任超級單純:顯示 `viewStore.count` 的值,然後在按鈕被點擊時,`send` 一個 `.increment` 或 `.decrement` 的 Action。View 本身完全不知道也不關心按下去之後數字會怎麼變。它就是個聽話的木偶。
好吧,所以那些專有名詞到底是什麼鬼?
OK,看完 Code 我們再回來講理論,你可能會更有感覺。TCA 的核心就四個東西,搞懂了就通了。
- State:單一真相來源
就是你這個功能需要用到的所有狀態。想像一下,所有變數,不管是計數器的數字、輸入框的文字、開關是開是關,全部都集中管理在這個 `State` struct 裡。好處是,你不用再東一個 `@State`、西一個 `@StateObject`,搞到最後自己都不知道哪個才是對的。 - Action:所有可能發生的事
這超重要的。Action 代表了使用者或系統「所有可能觸發的行為」。使用者點了按鈕?送一個 `.buttonTapped` Action。API 回應成功了?送一個 `.apiResponse(.success(data))` Action。它就像是在對系統喊話:「嘿!這件事發生了喔!」但 Action 本身不包含任何邏輯。 - Reducer:功能的大腦
这家伙就是老大。Reducer 是一個 function,它接收目前的 `State` 和一個傳進來的 `Action`,然後決定 `State` 應該變成什麼樣子。所有、所有、所有的業務邏輯通通都在這裡。所以當你想知道「為什麼計數器會加一?」你只要去看 Reducer 裡 `case .increment:` 那段 code 就行了,答案保證就在那,絕對不會在其他地方。 - Store:運轉一切的引擎
Store 就像是黏合劑,把 State、Action、Reducer 全部包在一起變成一個可以運作的物件。你在 View 裡面會持有一個 Store,然後透過它來讀取 State(給畫面顯示)和發送 Action(回應使用者操作)。
整個流程就是一個完美的閉環:View 讀取 State ➡️ 使用者操作,View 發送 Action ➡️ Store 把 Action 丟給 Reducer 處理 ➡️ Reducer 改變 State ➡️ Store 偵測到 State 變了,通知 View 更新。 這就是所謂的單向數據流,清清楚楚、明明白白。
跟以前的 MVVM 到底差在哪?
在我遇到 TCA 之前,我跟台灣大多數 iOS 開發者一樣,應該都是 MVVM 架構的信徒吧。用 `@StateObject` 或 `@ObservedObject` 建立一個 ViewModel,然後在裡面放一堆 `@Published` 的屬性跟一堆 function。能動嗎?當然能。但專案一大,我就開始精神錯亂了。
我每天都在問自己這些問題:
- 這段商業邏輯應該放在 A ViewModel 還是 B ViewModel?
- 這個共用的狀態,到底該由誰來「擁有」?
- 這個 function 到底更新了哪些 `@Published` 屬性?天啊,一個 function 改了五個地方,我怎麼知道畫面會怎麼跳...
結果就是 ViewModel 越來越肥,或者為了拆分,搞出好幾個 ViewModel,它們之間還互相傳遞、互相影響,最後就變成一團義大利麵。說真的,這可能是我自己功力不夠,但這種混亂感真的很消磨熱情。😫
我發現最近在台灣的 iOS 社群,像 iPlayground 或一些開發者的 study group,討論 TCA 的熱度也越來越高,就是因為它漂亮地解決了這些痛點。我們來做個簡單的比較:
| 我以前寫的 MVVM (混亂版) | 用了 TCA 之後 | |
|---|---|---|
| 狀態放哪? | 到處都是 `@Published` 屬性,有時候 ViewModel A 有,ViewModel B 也有,搞不清楚誰是老大。 | 就一個地方:`Store` 裡的 `State`。唯一的真相來源,清楚明白。 |
| 邏輯放哪? | 散在各個 `func` 裡,一個 func 可能改好幾個狀態,而且 View 還可以直接呼叫。 | 全部!通通都在 `Reducer` 裡面。想找邏輯?就去那裡,沒別的地方了。 |
| 怎麼更新狀態? | `viewModel.count += 1`、`viewModel.isPresented = true`...想到哪改到哪,很自由,也很危險。 | 只能 `viewStore.send(.someAction)`。多打幾個字,但換來的是 100% 的可追蹤性。 |
| 副作用 (打 API) | 通常就在 ViewModel 的某個 function 裡 `Task { ... }`,然後在裡面直接改 `@Published` 屬性。 | Reducer 會回傳一個 `Effect`,專門用來處理副作用。邏輯跟副作用是分開的,超乾淨!(這個今天先不細講,但這是精華) |
| Debug 的痛苦指數 | 😩😩😩 (這值是哪裡改的?為什麼會是 nil?我誰?我在哪?) | 😊 (哦,就這個 Action 進來,所以 State 變成這樣,結案。) |
那...有什麼缺點或是不習慣的地方嗎?
當然有。天下沒有白吃的午餐。
老實說,一開始真的有點綁手綁腳。你要學會新的思考方式,不能再隨心所欲地 `self.count += 1` 了。所有事情都要乖乖走「發送 Action -> Reducer 處理」的流程。寫一個簡單的功能,程式碼量看起來好像還變多了,要定義 State、定義 Action,好囉唆。
這點在美國的 SwiftUI 開發者社群也很多人討論,有些人覺得是「為了架構而架構」,有點過度設計。但我自己的體會是,你犧牲了一點點初期的方便,換來的是整個專案長期的健康、可預測性和穩定性。當你的 App 複雜到一定程度,或是團隊成員一多,這個「囉唆」的代價就變得超級值得。它建立了一套共通的語言,不管誰來接手,都能快速看懂程式碼的脈絡。
所以,TCA 值得學嗎?我自己是覺得,超值得。它強迫你用一種更嚴謹、更函數式的方式去思考 UI 開發,久了之後反而會覺得很自由,因為你對你的 App 有完全的掌控感。再也不用擔心有什麼「看不見的手」在背後偷偷修改你的狀態了。
當然,這只是 TCA 的冰山一角。後面還有更精彩的,比如如何組合多個 Reducer、如何處理 Navigation、如何優雅地寫測試...這些才是它真正發光的地方。之後有空再慢慢聊囉!
換你說說看了!
對於已經在用 TCA 的朋友,你覺得最爽的「啊哈!」瞬間是什麼?是對 Reducer 的掌握,還是對 Effect 的應用?
對於還在用 MVVM 或其他架構的你,最大的痛點又是什麼呢?是肥大的 ViewModel 還是混亂的狀態管理?
在下面留言分享你的經驗吧!👇
