Swift Redux 架構完整指南:從 Reducer 到 Middleware 的狀態管理實踐

引言:為什麼需要 Redux? 在 iOS 開發中,隨著應用規模擴大,狀態管理逐漸成為最具挑戰性的課題。當多個 View 需要共享狀態、狀態變化難以追蹤時,應用很容易陷入混亂。 Redux 作為一種可預測的狀態容器,最早在 JavaScript 生態系中流行,如今也廣泛應用於 Swift/iOS 專案。本文將深入介紹 Redux 架構的核心觀念,包含: Reducer(減少器):狀態更新的核心邏輯 Store(儲存區):應用的單一狀態來源 Action(動作):描述「發生什麼事」的指令 Middleware(中介層):處理非同步與副作用 Redux 核心架構概覽 架構組成 flowchart TB View[View / SwiftUI] Store[Store<br/>單一狀態來源] Reducer[Reducer<br/>純函數] State[State<br/>應用狀態] Action[Action<br/>動作描述] Middleware[Middleware<br/>非同步處理] View -->|1. dispatch| Action Action -->|2. 觸發| Middleware Middleware -->|3. 可能派發新 Action| Action Action -->|4. 傳遞| Reducer Reducer -->|5. 計算| State State -->|6. 更新| Store Store -->|7. 觀察 @Published| View style Store fill:#4ade80 style Reducer fill:#60a5fa style Middleware fill:#f97316 架構特性: ...

August 21, 2025 · 12 min · Peter

解決 API 回應中的 BOM (Byte-Order Mark) 字元問題

問題背景 最近在開發過程中遇到一個詭異的問題:呼叫某個 API 後,某個常數 name 的值居然是 nil,但從 raw data 看起來明明有值。 症狀檢查清單: ✅ Console 印出 raw data 看起來正常 ✅ jsonDecode 解碼成功 ✅ Enum 對應的 JSON key (_Name_Ch) 完全相同 ✅ 瀏覽器中直接訪問 API,name 確實有值 ❌ Swift 中取得的 name 卻是 nil 經過反覆檢查,終於發現問題根源:不可見的 BOM (Byte-Order Mark) 字元。 什麼是 BOM? BOM (Byte-Order Mark),中文稱為位元組順序記號,是一個不可見的 Unicode 字元,用於標示文字檔的編碼位元組順序。 常見的 BOM 字元: UTF-8 BOM: 0xEF 0xBB 0xBF (Unicode: U+FEFF) UTF-16 BE BOM: 0xFE 0xFF UTF-16 LE BOM: 0xFF 0xFE 問題診斷 根據問題分析,name 為 nil 的原因是: ...

May 15, 2024 · 3 min · Peter

iOS : 記憶體管理

在iOS app中,記憶體管理是基於引用計數模型運作。當創建一個物件的時候,記憶體會在Heap(堆)上分配,並將其引用計數設置為1。隨著其他物件對此物件建立強引用(strong reference),其引用計數會增加1。 iOS : 記憶體管理 在iOS app中,記憶體管理是基於引用計數模型運作。當創建一個物件的時候,記憶體會在Heap(堆)上分配,並將其引用計數設置為1。隨著其他物件對此物件建立強引用(strong reference),其引用計數會增加1。 相反的,如果擁有物件的持有者放棄了強引用,引用計數將會減少1。一旦引用計數變為0,該物件的記憶體就會被自動釋放。 當啟用了ARC(Automatic Reference Counting)自動引用計數功能的編譯器編寫程式碼時,編譯器會分析你創建的引用,並自動插入對底層記憶體管理機制的調用,我們無須手動設置引用計數。 隨著自動引用計數(ARC)的引入,我們只需要在引用物件時指定所有權的類型: 強引用(strong reference):確保被引用的物件只要引用仍然有效,就會一直保留在記憶體中。例如,我們宣告一個控制器屬性 strong var myView: UIView,表示控制器強引用 myView 物件,直到控制器釋放之前,myView 都會存在記憶體中。 弱引用 (weak reference):對被引用的物件的生存期沒有影響。例如,我們宣告一個閉包中的局部變數 weak var capturedView: UIView,表示閉包弱引用 capturedView 物件,即使 capturedView 被釋放,閉包中的 capturedView 也會變成 nil 而不會崩潰。 非擁有引用 (unowned reference):與弱引用類似,對被引用的物件的生存期沒有影響,但與弱引用的不同之處是,非擁有引用 預期總是擁有 「非 nil 的值」,**ARC 不會自動將其設置為 ****nil**。例如,我們宣告一個子視圖中的屬性 unowned var parentViewController: UIViewController,表示子視圖非擁有引用父控制器,而父控制器通常擁有比子視圖更長的生存期,因此子視圖可以安全地訪問父控制器。 需要注意的是: 當被引用的物件被釋放時,弱引用會被設定為 nil,而非擁有引用則會變成一個懸浮指標 (dangling pointer)。向懸浮指標發送訊息會導致程式崩潰。 使用非擁有引用時,要確保另一個物件擁有相同的或更長的生存期,避免懸浮指標問題。 簡單來說,在以下情況下使用不同的引用類型: strong: 當你想要確保物件一直存在,直到不再需要它為止; weak: 會在被引用的物件被釋放時自動設為 nil,因此它們可以用來避免循環引用。例如,在代理關係中,委托者通常會使用弱引用來引用代理,這樣當委托者被釋放時,代理也不會被保留。; unowned: 不會在被引用的物件被釋放時自動設為 nil,因此它們只能用於指向那些生命週期一定會比引用它的物件長的物件。例如,子視圖可以使用非擁有引用來引用父視圖,因為父視圖通常會比子視圖存在得更久。 assign: 當你只需要一個指向簡單數據的指標,不需要跟踪它的生命周期。 使用 weak 和 assign 引用可以避免循環引用,即兩個物件互相引用導致彼此都無法被釋放的情況。 ...

December 20, 2023 · 2 min · Peter

iOS中的associated type和associated value是不同的概念

它們在使用和用途上有很大的區別。讓我們來解釋這兩者之間的差異: iOS中的associated type和associated value是不同的概念 它們在使用和用途上有很大的區別。讓我們來解釋這兩者之間的差異: 是的,iOS中的associated type和associated value是不同的概念,它們在使用和用途上有很大的區別。讓我們來解釋這兩者之間的差異: Associated Type(關聯類型): Associated type是Swift中協議(protocols)的一個特性。 關聯類型允許在定義協議時指定一個或多個類型,但不需要提供具體的實現。 具體遵循(conforming)該協議的類別或結構體需要提供關聯類型的具體實現。 關聯類型使協議更具通用性,因為它可以適應不同的類型。 protocol OrderQueue { associatedtype Order // 定義一個關聯類型 mutating func enqueue(_ order: Order) mutating func dequeue() -> Order? } struct OrderQueueImplementation: OrderQueue { // 實現OrderQueue協議,指定關聯類型為OrderStatus typealias Order = OrderStatus private var orders: [OrderStatus] = [] mutating func enqueue(_ order: OrderStatus) { orders.append(order) } mutating func dequeue() -> OrderStatus? { return orders.isEmpty ? nil : orders.removeFirst() } } ...

October 27, 2023 · 1 min · Peter

iOS如何把專案打包framwork後也讓原本的pod/套件包入framework ?

工作需要把整個app包成一個SDK讓另一個app引用,所以要把原本專案的套件也包進去,不然在打包framework的時候,import套件那一行會報錯. iOS如何把專案打包framwork後也讓原本的pod/套件包入framework ? 工作需要把整個app包成一個SDK讓另一個app引用,所以要把原本專案的套件也包進去,不然在打包framework的時候,import套件那一行會報錯. 假設你的專案叫”B”,打包的framework取名為BPack,那麽在Podfile的部分要加入framework自己的套件引用,原本的專案裝什麼pod,framework如果有用到該套件也要跟著裝,以SnapKit為例,作法如下: target ‘B’ do Comment the next line if you don’t want to use dynamic frameworks use_frameworks! pod ‘SnapKit’, ‘~> 5.6.0’ Pods for B end target ‘BPack’ do Comment the next line if you don’t want to use dynamic frameworks use_frameworks! pod ‘SnapKit’, ‘~> 5.6.0’ Pods for B Pods for BPack end另外,在Build Setting的部分好像也要做一下設定: 如果你包的是動態庫,那麽在framework target的Build Settings: Linking -> Mach-O Type應該要選擇:Dynamic Libray Mach-O這部分不太確定有沒有影響,不過我這樣選是可以Run App Successful. 有錯麻煩請指正,感謝! ...

September 22, 2023 · 1 min · Peter

RxSwift -bindViewModel

第69行cell.configure一直進不去 RxSwift -bindViewModel 第69行cell.configure一直進不去 原因如下: required init?(coder aDecoder: NSCoder) 是 UIKit 控制器初始化的一部分,尤其是從 Storyboards 或 Xibs 中加載時。如果你正在嘗試從 Storyboard 或 Xib 中加載你的 ZooViewController,那麼就需要實現這個初始化方法。在你的程式碼中, required init?(coder aDecoder: NSCoder) { fatalError(“init(coder:) has not been implemented”) }你用 fatalError("init(coder:) has not been implemented") 拋出了一個執行時期的錯誤。 如果你確實不需要從 Storyboard 或 Xib 加載,你可以避免使用 ZooViewController 作為 Storyboard 或 Xib 的類別,或者避免透過 Storyboard 或 Xib 實例化 ZooViewController。在這種情況下,你的 init(coder:) 方法將永遠不會被呼叫,因此你的 fatalError 也不會觸發。 如果你需要從 Storyboard 或 Xib 加載 ZooViewController,那麼你需要提供 init(coder:) 方法的實現。這可能意味著你需要提供一個預設的 ZooViewModel,或者提供一種方式來設定 ZooViewModel,如下所示: required init?(coder aDecoder: NSCoder) { self.viewModel = ZooViewModel() super.init(coder: aDecoder) }最後從Main.storyboard的viewController冠上我要的ZooViewController, 問題就解決了.

July 21, 2023 · 1 min · Peter