SwiftUI Composable Architecture 入門:認識模組化架構設計與狀態管理概念

Published on: | Last updated:

最近被一個 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 本身完全不知道也不關心按下去之後數字會怎麼變。它就是個聽話的木偶。

一個極簡的 TCA 功能模組範例程式碼
一個極簡的 TCA 功能模組範例程式碼

好吧,所以那些專有名詞到底是什麼鬼?

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 更新。 這就是所謂的單向數據流,清清楚楚、明明白白。

範例計數器 App 在手機上的實際畫面
範例計數器 App 在手機上的實際畫面

跟以前的 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 還是混亂的狀態管理?

在下面留言分享你的經驗吧!👇

Related to this topic:

Comments

  1. Guest 2025-09-05 Reply
    欸,這篇好像在說什麼程式架構?我兒子最近也在學寫程式,不過我聽得有點暈。這TCA聽起來好像很厲害,不過真的有這麼複雜嗎?孩子他爸說要多學學新東西,不過我還是有點擔心啦。
撥打專線 LINE免費通話