SwiftUI TCA架構優化App維護性與開發效率實測分享

核心行動建議 - 快速強化 TCA 架構下 SwiftUI App 的維護性與開發效率

  1. 列出現有 reducer 與 state 結構,7 天內完成模組劃分並標記高頻率協作區塊。

    明確區隔功能邊界,減少跨模組耦合,可縮短後續重構時間至少 20%。

  2. 檢查所有 action 派發點,每個頁面不超過三種高頻互動設 debounce 處理。

    降低 store 接收無效操作壓力,有效防止性能瓶頸並優化用戶體驗。

  3. *活用 TestStore 寫測試*,每個核心 feature 至少覆蓋主要流程與異常狀況各一次。

    *提升測試信心*,及早發現設計問題且日後改動可即時驗證行為一致性。

  4. *預留觀察機制或使用 Observation API*,新功能上線前先量測狀態追蹤資源消耗。

    主動掌握性能變化,使架構能隨平台升級靈活調整,不易被技術債困住。

掌握 SwiftUI TCA App 真實應用場景與進階挑戰

前兩篇聊過TCA(The Composable Architecture)的概念,大致知道原理怎麼一回事,還順手搞了一個BMI小App來跑一下,當作是練基本功啦。但說真的,這些「基礎」離現實中的應用場景,其實……落差超大,有夠多眉角。不如這次直接上真刀:把Bako這款情緒追蹤App拆給你看。嗯,你要覺得好奇可以先去翻GitHub(https://github.com/mrezkys/Bako);也能試用一下成品感受下氛圍感(https://apps.apple.com/id/app/bako-mood-tracker/id6741732090),我自己體驗後其實有點意外。

想一層層拆,就不能再只談什麼「單純狀態管理」。具體來說,Bako要面對的複雜度,是那種 - 導覽流程極長、資料流動混亂到常常會讓人頭昏眼花的等級;協調每個小功能,全靠背後嚴謹架構撐著。啊,好像繞遠了。以下這篇,就是帶你摸清幾條核心進階路線,包括:

- 怎麼用 `NavigationStackStore` 跟 `StackState` 做多層巢狀甚至臨時分支的導覽
- 多步驟使用者流程裡,某些責任怎樣委派出去,不然各自爆炸
- SwiftData 整合跟依賴注入到底哪裡容易踩坑
- 子Reducer靈活切換、搭配選擇性管理,是真的處理了複雜狀態而不僅堆if/else
- 面對副作用與非同步丟回主流程時,要記得哪些死結很容易踩進去
- 效能考慮部分,有哪些小細節忽略就會GG

目前Bako帶給人的觀察經驗是:它就是一個典型「引導性操作」型App,表面溫和但內部機制反覆咬人。例如:

1. 首次啟動所謂 Onboarding,那其實…說短不短。
2. 回到 Home Screen,就直球塞近期心情概況。
3. Tracker 功能,是你每日必走心情檢查題。
4. Category Selection 才見真章,要選快樂還是不爽?同時分類又不可失誤。
5. Emotion Selection 時交互設計做得挺講究,每個情緒按鈕都有特殊觸感提醒。
6. 補充 Form 欄位時,「寫日誌、填活動地點之類的小瑣事」不知為何都沒那麼簡單,寫著寫著容易出戲。

每個環節需要克服的不僅僅是程式碼整齊,更是一種──欸,到底要把多少狀態暴露在畫面上才剛剛好?怕打太散,但塞太多東西又叫人喘不過氣。坦白說,只玩Sample App學不到這些感覺。但看完今天內容,大概會理解TCA那堆抽象介面和解耦套路,在複雜專案碰壁之後,到底該往哪走才比較像真的開發現場,不只是教科書答案啦。

認識專案架構,學會建立高可維護的 Feature-Based 專案

Bako 專案那個整體架構,其實說穿了,就頗有種很教科書式的 feature-based 分工吧,不過用起來 - 老實講 - 還算舒服啦。有時候做開發流程,不就是那些:例如「成功後顯示確認訊息,然後請你乖乖回首頁」,像這一套,你到哪間公司都會撞見。

回頭仔細看 Bako 的專案結構,其實拆得滿細的。例如最上層 App/ 目錄,它就把核心應用場域擺在第一線。這邊檔案名稱也直接明瞭(怎麼說呢?):像是 BakoApp.swift 負責掌控全局入口,然後又有個 AppReducer.swift 綁定所有功能的 Root reducer。接著你還可以看到 AppView.swift(主導航頁),或是像 AppDependencies.swift 那種管依賴注入的小精靈……對了,AppModels.swift 是負責 SwiftData 的模型註冊,最後一份則是管路由的 AppRoute.swift。總之,每份資料扛著各自的位置,但交織起來變成應用程式中控台。

再跳進 Features/ 夾,看起來更有組織感。它是一個又一個針對場景封裝好的模組:Onboarding 當然是新手首輪、Home 拉出儀表板日常;Tracker 用於每一天心情追蹤,SelectCategoryFeeling、SelectFeeling 則分別鎖定大分類和細項選擇(喂,有時這階段不知該按哪顆)。FormFeeling 進去要補脈絡,再送出就推進 SuccessSubmitFeeling 顯示完成確認。還有 DetailFeeling 管你之前記錄怎樣隨時查閱。每顆功能下面一般都固定塞著 Reducer 和 View 幾組檔案,好讓團員彼此分界作業比較沒交集、相安無事。

Core/ 資料夾咧,就是把大家會重複動到的玩意全部堆齊,有 Common 組件與工具函數, Models 跟 Enums 集中打包,加上一塊叫 Design 的地盤拿來收設計系統跟圖資。有點意思的是 - 這種極力走高內聚低耦合嘛 - 如果將來超過二十種功能混在裡面,也還能撐住。不只協作較為省心,要搬某幾個特性獨立出去搞副產品、甚至轉製成另一套 app,其實彈性比預期的大。不知怎地,又想起之前踩坑才恍然理解:結構清楚帶來維護與擴編空間,不誇張,是打基礎真的好重要啊。

認識專案架構,學會建立高可維護的 Feature-Based 專案

使用 NavigationStackStore 實現多頁流程和路由管理

老實講,TCA(The Composable Architecture)裡頭那個 root reducer 嘛,其實就是把所有 feature 狀態全扛在一處,一次處理到底。嗯……它設計起來大概像這樣:

@Reducer
struct AppReducer {
@ObservableState
struct State: Equatable {
var path = StackState<Route>() // 這是導航堆疊
var home: HomeReducer.State? // 可選欸,要 onbaording 後才會出現
var tracker: TrackerReducer.State? // 也是可選,隨時才用得上
var onboarding: OnboardingReducer.State // 必備,進來時一定有的狀態
var selectCategoryFeeling: SelectCategoryFeelingReducer.State?
var selectFeeling: SelectFeelingReducer.State?
var formFeeling: FormFeelingReducer.State?
var successSubmit: SuccessSubmitFeelingReducer.State?
var detailFeeling: DetailFeelingReducer.State?
var about: AboutReducer.State?
}


enum Action {
case path(StackAction<Route, Never>)
case home(HomeReducer.Action)
case tracker(TrackerReducer.Action)
case onboarding(OnboardingReducer.Action)
// ……底下還有一堆其它 action 要管啦
}
}


說穿了嘛,多數子狀態其實都被設成「可選型」。這點我第一次看到時還小愣一下,畢竟 TCA 很注重那種有需才生、沒事就回收的設計。話題拉回來 - 像 `onboarding` 這種常駐不動的東西,就乾脆直接用非 optional,因為永遠不可能消失;但類似 `tracker`、`formFeeling` 啦、那種只要需求到了才 pop 出現的新功能,都藏在 optional 變數裡面比較划算。

不得不說,如此安排真的很省記憶體空間,又好管理。你要新頁面?給你動態生!不需要就 nil 掉收工。

至於進階導航模式,其實也頗有意思。有一派朋友學 Bako 操作,他們就倚賴 TCA 的 `NavigationStackStore` 搭配程式碼直接打點整個導覽流程。

AppView 那邊是怎麼包裝 NavigationStackStore 的?瞧:

struct AppView: View {
let store: StoreOf


<pre><code class="language-css"><pre><code class="language-css">var body: some View {
NavigationStackStore(
store.scope(state: \.path, action: \.path)
) {
// 根視圖邏輯大致長這樣……
WithViewStore(store, observe: \.home) { viewStore in
if let homeState = viewStore.state {
HomeView(store: store.scope(state: \.home!, action: \.home))
} else {
OnboardingView(store: store.scope(state: \.onboarding, action: \.onboarding))
}
}
} destination: { route in
// 依據 Route 切換顯示畫面哦!
switch route {
case .tracker:
TrackerView(store: store.scope(state: \.tracker!, action: \.tracker))
case .selectCategoryFeeling:
SelectCategoryFeelingView(store: store.scope(state: \.selectCategoryFeeling!, action: \.selectCategoryFeeling))
// 還有其他分支自己加......
}
}
}
}


老是聽人問,「所以真的不用煩 `NavigationLink` 綁定嗎?」答案超簡單,只要撥弄 `path` 狀態,那些畫面的跳轉推疊都自動跟著跑。而且更帶感的是,不止根本省掉冗長參數傳遞,每層 route 就是一條清楚的路徑。

至於 Route,本尊如下:

enum Route : Equatable, Hashable {
case tracker
case selectCategoryFeeling
case selectFeeling
case formFeeling
case successSubmit
case details(EmotionModel) // Routes 其實也能順便丟資料過去
case about

設計多步驟資料流與功能間有效溝通技巧

在 TCA 這種架構下,每一個 feature 說白了都不直接動手控制頁面該怎麼跳,它只會拋出「我希望去某個地方」的想法,之後要去哪、流程細節,讓上層家長來煩惱。其實這做法...有那麼點像跟上司丟單子請示,但自己的範圍裡先別插手太多,所以功能元件就變得比較好複用,也容易被單獨拉出來測試,不怕牽連。

## 多步驟流程與資料怎麼交流

你看情緒記錄那套操作,就是很標準的 TCA 多步驟、多畫面間資料傳遞縮影,而且流向其實蠻繁複。

### 整體走法
Home → Tracker → Category Selection → Emotion Selection → Form → Success → Home
照這順序,每一格是獨立功能(feature),偏偏它們還得互相給資料、一起合作才能完成整個歷程。簡單講一下其中關鍵段落:

### 功能模組間資訊串接

**第 1 步:選類別**swift
@Reducer
struct SelectCategoryFeelingReducer {
@ObservableState
struct State: Equatable {
var selectedEmotionCategory: EmotionCategory?
}


enum Action: Equatable {
case selectCategory(EmotionCategory)
case continueButtonTapped
case delegate(Delegate)css
enum Delegate: Equatable {
case routeToSelectFeeling // 單純狀態存著即可—不用多帶東西。
}
}
}


**第 2 步:勾選情緒**
// 跳場景時交由 AppReducer 管控
case .selectCategoryFeeling(.delegate(.routeToSelectFeeling)):
let emotions = state.selectCategoryFeeling?.selectedEmotionCategory == .positive ?
positiveEmotions : negativeEmotions
state.selectFeeling = SelectFeelingReducer.State(emotions: emotions) // 過濾過適合當前的清單,下放過去。
state.path.append(.selectFeeling)
return .none

**第 3 步:表單填寫,把前一步選到的塞進來**
case .selectFeeling(.delegate(.routeToFormFeeling)):
if let selectedIndex = state.selectFeeling?.selectedEmotionIndex,
let selectedEmotion = state.selectFeeling?.emotions[selectedIndex] {
state.formFeeling = FormFeelingReducer.State(
selectedEmotion: selectedEmotion // 明確傳下剛才鎖定那個情緒值
)
state.path.append(.formFeeling)
}
return .none


### 資料邊界嚴謹設計,有點神奇

每隻 feature 初始就是給它「正好需要」又經處理過的資訊,例如 `SelectFeelingReducer` 就拿到自己這步該呈現的情緒種類和數量;等到下一步,`FormFeelingReducer` 就只剩下最終圈選出來那一筆要處理。有趣吧?噢...原因很多 - 譬如:
- **依賴清楚明白** - 不必糾結到底 feature 要的是什麼原料,全程一目了然;
- **型別顧周全** - 不怕亂餵型別或傳錯東西搞爆,安全;
- **單元測試友善** - 隨時灌各種組合測測看環節正不正常;
- **高重複利用率** - 因為需求專注又接口固定,可輕易抽換不同任務、或外包給其他場景使用。

講句老實話啦,如果你沒用明確框架,其實 SwiftUI 很容易養成把所有東西塞 environment object 裡跑,綁 binding 一堆線在各 component 間晃,那亂七八糟難追根本家常便飯。採用 TCA 後,資料流都白紙黑字寫在代碼內,看調度邏輯瞬間清爽許多。有事發生哪個環節,就知道往哪查比較快...

## 合併 SwiftData 做保存這回事

Bako 那個 app,本身也用 SwiftData 當持久化方案。但尷尬的是,不可以從 view 自己偷偷存檔、讀檔,而是規規矩矩通過 TCA 的 dependency system 把存取拉開分工。坦白說,弄久了大概會習慣,多一層抽象隔絕其實反而省力......不然偶爾誰都想偷懶直呼命令吼。

設計多步驟資料流與功能間有效溝通技巧

導入 SwiftData 和依賴注入,打造乾淨持久化結構

講白了,這段說的是一種蠻標準的 SwiftData 模型設計邏輯。整個模型建構流程是有步驟區隔的,啊 - 你大可不必一口氣搞定全部。像 EmotionModel 其實只要初始時輸入 date、feel、iconType 跟 category 就好,其他東西(譬如 journal、activities、place)後面再用表單慢慢補都可以。有些人剛開始看到欄位這麼多會皺眉,但其實步驟切開反而鬆一口氣吧。

然後提到 SwiftDataClient,也挺有趣的齁!簡單來說,就是做依賴注入,把所有跟 SwiftData 有關的活打包起來放在一個「可被 reducer 用」的工具裡頭。這種手法不是為了花樣多,其實背後蠻多好處:例如,你想做 mock 測試就超級方便;或者哪天業務邏輯要微調,存儲層抽換一下也行,不太需要動核心流程。此外,把錯誤集中統一管控啦,再加上主線程安全性都顧到了,每個方法都明確走在 @MainActor 上。不知道為啥想到細水長流...可能是封裝得還滿縝密(咦?)。實際寫法他們也是直接抓 Schema 跟 ModelConfiguration 做出 ModelContainer,以便 context 與 save 方法能順利調度。

繼續講下去,這份依賴會交給 reducer 用。例如 FormFeelingReducer 那邊就會吃進 swiftDataClient。當執行 .saveEmotion 時,其步驟分成兩塊 - 先同步更新 emotion 的幾項值,比如 journal、activities 和 place 等等再含著 date,一股腦地全塞進去。緊接著才利用 .run effect 執行資料存儲,說白話點就是把複雜或「會拖時間」的事情拉出去異步處理。effect 本身則靠 swiftDataClient 提供 context 對象插新資料,再呼叫 save() 完成真正寫檔收尾。如果過程發生問題,也沒有馬虎處理:狀態結果會透過 send(.emotionSaved) 傳回給管理端,有錯就記錄與捕捉,看起來算蠻扎實。【總之】副作用控制其實就是:第一層照表操課把數據拉齊(同步),第二層 .run 出門辦大事(非同步),再將資訊丟給效應完成所有流程。有碰到更大型狀態問題?嗯...可以日後搭配其他策略漸次優化嘛,好啦,先分享到這邊。

善用 Optional Child Reducers 處理複雜 UI 狀態切換

進階的 Bako 設計模式其實主要都體現在幾個看似不起眼、但細節爆炸的手法裡,然後我在這邊挑點出來唸一下 - 嗯,其實挺常見但做起來又讓人一頭霧水。

### 可選子 Reducer 跟組合應用
像是下面那個範例,`SelectFeelingReducer` 直接搭配 `.ifLet` 操作符去搞定「有沒有子狀態」的問題(如 `formFeeling`),反正如果沒資料它就全自動把 action 丟水溝,有值才真正往下派送,也算省心。
@Reducer
struct SelectFeelingReducer {
@ObservableState
struct State: Equatable {
var selectedEmotionIndex: Int?
var activeCircleIndex: Int?
var currentOffset: CGSize = .zero
var lastOffset: CGSize = .zero
var emotions: [EmotionModel]
var formFeeling: FormFeelingReducer.State? // 可選子 reducer 狀態
}css
var body: some ReducerOf
 {
Reduce { state, action in
// 處理本身的事情嘛。
}
.ifLet(\.formFeeling, action: \.formFeeling) {
FormFeelingReducer() // 需要才組成子 reducer,不強求。
}
}
}

`.ifLet` 超懶人,它會依據你 `formFeeling` 有沒有東西自己決定要不要繼續傳遞下去;沒內容全部忽略,一旦設值馬上生效,就算其他地方亂搞也不怕混亂了,狀態劃分變得乾脆不少。

### 複雜互動式 UI 狀態管理
拿情緒選擇畫面做例子,其實 TCA 架構可掌握超級多瑣碎又細微的互動紀錄,包括用戶滑過、拖曳或按到哪顆圓圈,全都能被拆細記住。資料結構大致如下:
struct State: Equatable {
var selectedEmotionIndex: Int? // 哪一個情緒目前被挑中?
var activeCircleIndex: Int? // 現在畫面亮的是哪顆圈圈?
var currentOffset: CGSize = .zero // 現正在拖拉累積多少距離?
var lastOffset: CGSize = .zero // 前次剩下未歸零偏移量,用來調節操作流暢感…
}

enum Action: Equatable {
case selectEmotion(Int)
case updateActiveCircle(Int?)
case updateOffset(CGSize)
case setLastOffset(CGSize)
}
所以即使邏輯變很複雜,只要集中丟進 reducer 過濾和判斷,其實都還維持得住預測性,你想驗證或模擬極端行為也不會卡死,很難崩壞掉(笑)。

### 狀態內推導型計算屬性
這部分... 我早期常常偷懶,把一堆 computed properties 堆在 view 層,結果每次檢查要連帶 UI 一起測。但是其實放回 observable state 裡寫推導資訊好處不少:只改底層資料自動變更衍生欄位,要校對或單元測試也都乾脆。
@ObservableState
struct State: Equatable {
var selectedDay: DayType = DayType.fromWeekday(Calendar.current.component(.weekday, from: Date()))
var selectedDate: Date = Date()
var isDatePickerPresented: Bool = false

<pre><code class="language-css">var checkInTitle: String {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let selectedDate = calendar.startOfDay(for: self.selectedDate)css
if calendar.isDate(selectedDate, inSameDayAs: today) {
return "Today's Check In"
} else if selectedDate > today {
return "Future Check In"
} else {
return "Past Check In"
}
}


var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, d MMM yyyy"
return formatter.string(from: selectedDate)
}
}
這類機制真的輕鬆不少,比如驗證日期計算完全不仰賴渲染層啦,有啥 bug 測一測 state 結構本身就知曉問題藏哪。

**原作者小聲 murmur:**
「以前老愛省事塞 UI 裡計算,好像方便,殊不知 debug 下去全世界雞飛狗跳… 後來真心推薦給自己走 state 層通通管。」

## 共用元件跟重用思路

TCA 不可能每次新場景就從零打造而且吧?肯定是找哪些東西抽公共,免得天天勞神。

### 與 Store 綁定式元件設計
比如說那個 `EmotionCirclesView` - 高度交互就是各種和 store 黏緊緊設計,每一次點擊、滑鼠移動全透過 store 回送觸發後續行為。簡直感覺不到解耦,但真的對於密集反饋場景很少會出包…
struct EmotionCirclesView: View {
var emotions:[EmotionModel]
@Bindable var store :StoreOf

func getScaleFactorForIndex(index:Int)->Double{
let normalScale=1.0;let centerScale=1.05;
let selectedScale=1.10;
if index == store.selectedEmotionIndex{return selectedScale}
if index == store.activeCircleIndex{return centerScale}
return normalScale;
}
var body : some View{
GeometryReader{geometry in
ZStack{
ForEach(Array(emotions.enumerated()), id:\.element.id){index,emotion in
Circle()
.foregroundColor(index==store.selectedEmotionIndex?.darkBlue:.lightestBlue)
.opacity(index==store.activeCircleIndex ? 1 : 0.7)
.scaleEffect(getScaleFactorForIndex(index:index))
.onTapGesture{store.send(.selectEmotion(index))}
}
}
}
}
}
大概就是如此綁死方式,如果偏向一定要求所有使用者行為精確可追蹤、component 本體明確知道 state 和 action 拿哪裡,那就只能無腦直插 store 啦!啊,有時候有點煩,但還是比臨時抓 event 更靠譜。

### 資料導向純展示型元件拆分
可是遇到單純 show 炫樣板、不沾任何「流程切換」需求時,我通常硬拆出 data-driven 的格式 - 意思就是剛需誰傳什麼給它,就只專心演戲,既靈巧又便宜。不牽涉外部狀態,不容易因狀況跑飛。
scss
struct EmotionCardView : View{
let emotion : EmotionModel
let onTap : () -> Voidcss
var body : some View{
VStack{
Text(emotion.feel)
Text(emotion.date , style:.date )
}.onTapGesture(perform:onTap )
}
}

// 在某頁比如 TCA 整合那塊可以直接怎玩:
EmotionCardView(emotion :emotion ){
store.send(.emotionCardTapped(emotion ))
}
諸如此類小組件,就沒和狀態工具(像 Store)套牢,可隨便抽換整併至任何 view 特化用途,用好了收收包,下回復用搬過來輕鬆得很。

**補充一嘴...**
「一般如果只是陳列表現 show,本能選 data-driven 建議優先啦。但萬一遇到工程隊對”精準存取/追蹤界面激烈有執念”,偶爾忍耐寫 tight-coupling 版本其實比較直接有效…話講完。」

善用 Optional Child Reducers 處理複雜 UI 狀態切換

活用 State 設計、Computed Properties 增強測試便利性

有時寫軟體,特別在「功能可重用」和「直接綁死特定任務」之間,真的是心煩意亂,你懂嗎?選哪邊都不是個完全沒有後遺症的解啊。

## 副作用與非同步操作
先拿 API 呼叫、資料庫存取來說──這類狀況讓大多數開發者的心思都卡在副作用處理上。像 Bako 這套,用法怎樣呢?下面直接用範例講清楚。

### `.run` Effect 模式

swift
case .saveEmotion:
guard var emotion = state.selectedEmotion else { return .none }


// 準備資料(同步)
emotion.journal = state.journal
emotion.activities = state.selectedActivity
emotion.place = state.selectedPlace
emotion.date = state.currentDate

// 處理副作用(非同步)
return .run { [emotion] send in
do {
try await MainActor.run {
let context = swiftDataClient.context()
context.insert(emotion)
try swiftDataClient.save()
}
await send(.emotionSaved) // 成功動作
} catch {
await send(.saveError(error)) // 錯誤動作(如有實作)
}
}
這招很常見,步驟就這四個:
1. 狀態會在 reducer 裡頭同步塞好。
2. `.run` effect 單獨扛起所有要等的事情,不阻塞主流程。
3. 一切有結果,再丟 action 回去接續做下一步。
4. 所有需要跟副作用連動的資料,在閉包裡都封一份,避免之後踩到舊值、爛掉整段狀態。

### UserDefaults 整合

然後 Bako 還會透過 UserDefaults 幫使用者記住一些偏好設定哦:

@Reducer
struct FormFeelingReducer {
@Dependency(\.userDefaults) var userDefaults

// 載入偏好設定
case .onAppear:
state.customActivities = userDefaults.stringArray(forKey: "customActivities") ?? []
state.customPlaces = userDefaults.stringArray(forKey: "customPlaces") ?? []

if let lastActivity = userDefaults.string(forKey: "lastSelectedActivity") {
state.selectedActivity = lastActivity
}
return .none

// 儲存偏好設定
case let .selectActivity(activity):
state.selectedActivity = activity
userDefaults.set(activity, forKey: "lastSelectedActivity")
return .none
}
UserDefaults 屬於所謂的「同步副作用」,通常讀寫速度也很快,所以不需要再拉出去丟給 `.run` effect 特別處理,老實說直接寫 reducer 就行。

## 效能考量及最佳實踐

### 狀態結構對效能的影響

swift
// 建議 - 僅保留必要且精簡的狀態欄位
@ObservableState
struct State: Equatable {
var selectedEmotionIndex: Int?
var emotions: [EmotionModel]
}


// 不建議 - 包含過多無關或頻繁變化的狀態,可能造成不必要 UI 更新
@ObservableState
struct State: Equatable {
var selectedEmotionIndex: Int?
var emotions: [EmotionModel]
var allUserData: [UserModel]
var debugInfo: String
}
想節省重畫浪費,其實不用貪多,把僅僅真正要給那頁看的東西放進去最好。如果把每次一點小東西就全打包給 view - 慘了,很快就變拖慢 UI 的元兇啦。

### 觀察最小必要範圍以提升畫面效率

// 建議 - 僅觀察有需要之內容
struct HomeView: View {
@Bindable var store: StoreOf<HomeReducer>
@Query private var recentEmotions: [EmotionModel]css
var body: some View {
// SwiftData 的 Query 負責有效監看資料庫異動
// store 控制 TCA 狀態改變監聽
}
}


// 不建議 - 過度廣泛地觀察全部狀態
WithViewStore(store, observe: { $0 }) { viewStore in
// 此寫法會訂閱所有狀態異動,即使和當前畫面無關
}
其實只看自己想查詢或顯示的小片段效果最佳,不然一下子監聽一堆跟目前畫面八竿子打不著的東西,浪費嘛。而 store+SwiftData `@Query` 配合用法,就是現今推薦主流哦!

### 利用 Optional Child Reducers 管控記憶體

// 建議 - 按需求載入 feature reducer 子流程並及時釋放
case .home(.delegate(.routeToTrackerView)):
state.tracker = TrackerReducer.State()
state.path.append(.tracker)
return .none

case .successSubmit(.delegate(.backToHome)):
state.path.removeAll()
return .none
恩,有些人弄半天還是忘了 memory 要讓它乾脆點回收,其實 NavigationStackStore 管理得算方便:只要導航路徑砍光,自然子視圖和對應資源一同消失,不用手忙腳亂另外 hack 清除記憶體,好險吧!

### 行為 Action 的細緻度

java
// 建議 - 以明確且用途專一的 action 定義業務邏輯觸發點
enum Action: Equatable {
case selectEmotion(Int)
case updateActiveCircle(Int?)
case updateOffset(CGSize)
case setLastOffset(CGSize)
}
css
// 不建議 - 定義過於抽象、難測試或型別不安全之 action
enum Action: Equatable {
case updateUI(UIUpdate)
case handleInput(Any)
}

基本上喔,如果你的 action 明確又單純,那日後 trace log 或自動測試真的輕鬆太多。要是哪天你才剛維護幾百列 code 就遇到什麼 Any-action 亂丟事件,看得人頭疼又怕 bug 滲漏,一堆麻煩事啦。

製作可重複利用的 TCA Store-aware 及 Data-driven 組件

說到 TCA(The Composable Architecture)應用,如果要在現實世界裡真的做出來 - 嗯,下面幾個模式倒是跑不掉,大概可以這麼分吧。

1. 導航既是狀態
複雜流程處理起來其實滿頭痛的。有時候,直接上 `NavigationStackStore` 搭配 `StackState` 管理就比較穩妥。這一招說白了,就是全部導航邏輯都變成程式層級能掌控 - 不是點按轉場,而是真的讓所有跳轉邏輯、路徑堆疊都變透明而且可測試。對,有些人嫌繁瑣,可你要自動測導航沒它很痛苦。

2. 功能委派
然後啊,各功能彼此要講話,還是得靠 delegate actions,而不是像以前一樣互相鉤死或亂丟 callback。看似多一道,但維持模組獨立重用又更順手,重點是你想寫單元測試、不被其他功能拖累,也才真的行得通。不少老開發者吃過耦合的虧...唉,懂的就懂。

3. 可選子 Reducers
有時候 feature 狀態根本不是每秒都用得上,那怎辦?將 feature 狀態弄成 optional 格式,不需要時根本連狀態資料也不用預留給它。只有真遇到需求、使用者敲開那頁面,它才進來活躍一下。結果呢?記憶體沒那麼燒,管理起來腦袋也少打結。

4. 依賴注入
什麼資料庫啦、什麼網路 client 通常別黏死一個版本。直接包成 dependencies 丟進系統裡,要換 mock 或新資源時也好抽換。此外,不同環境下執行只改配置,不需改原始碼。本來以為這種設計距離自己很遠,其實用了才發現方便,奇怪耶。

5. 副作用(Effect)模式
遇到非同步事情,多半就是 `.run` 把該拉回的東西弄乾淨,把結果以 action 型式回拋,好讓主架構收繼續後面的流程,也比較容易追蹤和管理副作用。而且類型明確、錯誤好抓,即便偶爾卡住,「咦?到底哪兒沒捕捉?」再追 log 不會太絕望。

總之嘛,上面這些算關鍵套路,不管你寫 TCA 還是哪套有 composability 的框架,多少都踩得到這些點。能寫全,也許工作就省心一些,不然就等著 Debug 大地遊戲吧...

製作可重複利用的 TCA Store-aware 及 Data-driven 組件

正確執行副作用及非同步操作提升資料一致性

把應用程式狀態弄得既小又明確,說老實話,這對複雜 APP 真的很要緊啊。有時候,看太多人直接往每個模組裡全塞一堆資料 - 欸?會亂掉啦。
**作者隨手感想:**
說起來,我也是掙扎好久才開始習慣這幾種設計路線。那陣子不只卡、還有點煩,可是現在再回頭看,真的在 TCA 開發上效率差很多。其實嘛,每種模式背後都有現場會遇到的麻煩點對照著,漸漸地也能搞懂它們彼此到底怎麼搭配。一來一往、邊修邊跑,有時就蠻神奇的 - 搭得起來。

## 下一步

呃…TCA(The Composable Architecture)的系列先告個段落吧(笑),但是人越學越深,你總是會發現還沒碰到的坑、或者新的玩法。所以大概先記一下還想聊的大標題:

- **模組化架構(Modular Architecture)**:關於怎麼把一整包 TCA 大專案拆成不同 Swift 套件,其實利多不少耶。像是協作省力一些、維護程式碼乾淨不糾結,每次調東調西心情都平靜了。
- **API 整合(API Integration)**:你知道串網路、不管驗證還是資料同步,有些需求比表面複雜多了。有機會可以用 TCA 的 dependency 系統好好統整,不然分岔失控超頭痛。
- **進階測試(Advanced Testing)**:我想每個人都至少被異步或整合測試折磨過…。這一塊就是深入介紹各類測試策略,包括 UI 跟那些超囉嗦流程該怎麼混在一起驗證。

講認真,TCA 有彈性,也撐得起商規等級的大東西;但同時,就算今天只是從零做小玩具,也跑得很穩。我覺得啊,本系列列出來那幾套方法,如果你自己動手組一次,大概能理解苦心所在。最重要 - 拜託別一步登天,不妨紮紮實實打地基,真的有餘裕再慢慢加新武器,那效率跟心理壓力,都比較能承受。

**寫到這順便提醒一下:**
當初開 Bako 專案老實說常懷疑人生,一大堆功能反反覆覆一直串。如果不是 TCA 幫我整理結構,那層級跳來跳去肯定爆炸了。嗯... 前期蠻挫敗,但適應之後反倒愈走愈快,所以千萬不用怕碰壁;初學別逞強啦,從簡單功能一路推進,到最後擴展,大體上會變輕鬆,也不至於崩潰收場(笑)。

## 給讀者的一段話
嘿,你看到這裡其實讓我很感激。我嘛,人平常筆耕挺佛系,可是真的希望偶爾字裡行間,給你留下一點可借鏡或摸索痕跡。如果現在卡住,只是還沒走到豁然開朗的哪一天,下回看到更遠風景時,我們下次繼續聊。

優化效能:維持狹義狀態設計與健全元件拆分

我的名字叫做 Muhammad Rezky Sulihin。文章大致寫到這邊了,有點像是喘口氣啦。先跟你說聲謝謝,感激你抽空看完這些雜七雜八的內容。如果有什麼想法或者問題,都歡迎直接用電郵找我聊聊([. ](mailto:))。不管是關於我的英文書寫、對細節或論點準確性的疑慮,甚至單純回饋或補充,只要一句話,我都當作鼓勵收下。有時候一個小提醒就會讓我醒來,嗯,原來那裡還能再想想 - 未來,希望在更多其他主題下還能遇見你。

順便說一下,其實目前我是專注在行動 app 的開發,也正在念資訊工程的學士。剛從 Apple Developer Academy @IL 畢業沒多久,每天腦袋裡總繞著新點子跑。有什麼機會──無論合作專案、外包案子,或者正在徵才、提供實習與正職工作──只要覺得值得嘗試,都請聯絡我吧,我很想多探些不同路線。

祝大家還保有那份對世界新奇事物的追求,不要悶壞,也別停止摸索!

## 歡迎回饋

坦白講啊,這整篇算是我自己的學習紀錄,比較像自留地,但東西難免會有疏漏或誤差。我只是很單純希望能記下些過程,把心得分享出來,也許能多少幫上某個正好卡關的人。不介意給建議、更正錯處或任何提醒,我都非常期待而且誠懇地接受;偶爾反思、有時迷糊,在你的指正之下,也許哪天真的可以慢慢靠近更完整的自己。

Related to this topic:

Comments