學習 iOS 開發時,到了某個階段一定會遇到 RxSwift 或 Combine 這道牆。它們幾乎必定出現在徵才公告中,但打開程式碼後,看到 map、flatMap、sink一路串接的陌生語法,很容易感到不知所措。「明明用閉包和委派就能正常運作,真的有必要學這個嗎?」有這樣的疑問很自然。
今天先不談語法,從 為什麼會需要這類工具開始整理。理解原因後,語法自然就能跟上。
iOS App 其實是「事件處理機器」
拆解 App 的工作內容,大多都是回應事件。
- 使用者點選按鈕 → 切換畫面
- 網路回應抵達 → 更新清單
- 鍵盤出現 → 將輸入欄位往上推
- 文字欄位值變更 → 重新請求搜尋結果
問題在於,UIKit 會以 各自不同的方式傳遞這些事件。
| 事件來源 | 傳遞方式 |
|---|---|
| 按鈕點選 | target-action |
| 表格視圖捲動 | delegate |
| 網路回應 | completion handler(閉包) |
| 鍵盤出現 | NotificationCenter |
| 物件屬性變更 | KVO(Key-Value Observing) |
在同一個畫面同時使用這五種方式,在 iOS 中是再平常不過的事。同樣都是「回應事件」,程式碼卻分散成五種形式,因此要掌握單一畫面的行為,就得翻遍各處的視圖控制器。
RxSwift 和 Combine 要解決的第一個問題就是這個。它們將 所有事件統一成「隨時間流動的值串流」。按鈕點選、網路回應和鍵盤通知,都能透過相同介面處理。
靠回呼撐下去會遇到的兩道牆
牆 1:組合非同步工作
常見需求是:「同時請求個人資料與個人頁面的近期文章,兩者都抵達後再繪製畫面。」若用 completion handler 撰寫,會變成這樣。
var user: User?
var posts: [Post]?
func loadProfile() {
let group = DispatchGroup()
group.enter()
api.fetchUser { result in
user = try? result.get()
group.leave()
}
group.enter()
api.fetchPosts { result in
posts = try? result.get()
group.leave()
}
group.notify(queue: .main) {
guard let user, let posts else { /* 錯誤處理又該放在哪裡? */ return }
render(user, posts)
}
}
你必須動用 DispatchGroup、暫存變數和分散各處的錯誤處理。當請求增加到三、四個,再混入「A 完成後用結果請求 B」這類相依關係,巢狀結構就會失控。這就是所謂的回呼地獄。
在 Combine 中,同樣的需求可以這樣表達。
api.fetchUser()
.zip(api.fetchPosts())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion { showError(error) }
}, receiveValue: { user, posts in
render(user, posts)
})
.store(in: &cancellables)
「把兩者 zip 起來,在主執行緒接收,成功時通知,失敗時也通知。」程式碼幾乎照著需求讀就能理解,錯誤處理也集中在同一處。
牆 2:控制連續事件
想像搜尋欄位。每輸入一個字就呼叫 API,輸入「Swift」五個字就會送出五次請求。因此通常會加上這些條件。
- 只有停止輸入 0.3 秒後才發出請求(debounce)
- 若與上一個搜尋字詞相同,就不發出請求(去除重複)
- 發出新請求後,取消尚未完成的舊請求
若用 Timer 和旗標變數自行實作,計時器失效時機與請求取消時機彼此交錯,很容易產生錯誤。在 Combine 中,只要串接已驗證的運算子即可。
searchTextSubject
.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main)
.removeDuplicates()
.map { api.search(query: $0) }
.switchToLatest() // 收到新請求時自動取消上一個請求
.sink { results in render(results) }
.store(in: &cancellables)
重點就是這個。**不自行實作涉及時間的事件控制邏輯,而是組合已驗證的元件來解決。**這正是反應式程式設計價值最清楚的地方。
MVVM 綁定的標準元件
如上一篇文章所述,MVVM(Model-View-ViewModel)必須具備「ViewModel 改變時 View 會自動跟著改變」的綁定才算完整。但 UIKit 沒有內建綁定。填補這個空缺的是 RxSwift(RxCocoa),而從 iOS 13 開始,Combine 接手了這項角色。
viewModel.$isLoading
.sink { [weak self] in self?.spinner.isAnimating = $0 }
.store(in: &cancellables)
實務上有這麼多徵才公告要求 RxSwift 或 Combine,真正的原因就在這裡。因為在以 MVVM 為基礎的程式碼庫中,綁定層實際上就是由這兩個框架其中之一建立的。
RxSwift 和 Combine 有什麼不同?
兩者在概念上都是反應式程式設計工具。把它理解成 Observable 變成 Publisher,而 subscribe變成sink,只是名稱改變,並不為過。學會其中一個後,對照術語就能掌握另一個。不過,選擇標準仍然很明確。
- RxSwift:第三方程式庫。沒有 iOS 版本限制,透過 RxCocoa 提供豐富的 UIKit 綁定,長年累積的資料與社群是優勢。代價是增加外部相依性,並延長建置時間。
- Combine:Apple 的第一方框架。iOS 13 以上無須新增相依性即可使用,並能自然銜接 SwiftUI 的
@Published、ObservableObject。但 UIKit 綁定支援比 RxCocoa 較少。
新專案通常以 Combine 作為穩妥的預設選擇;加入既有程式碼庫已使用 RxSwift 的團隊時,則常會學習 RxSwift。
有了 async/await 之後,還需要它嗎?
Swift 5.5 的 async/await 出現後,常有人問「現在不用學 Combine 了吧?」這話只對了一半。對於以 **「一次請求、一次回應」**結束的單次非同步工作,async/await 好讀得多。上面的個人資料範例只要 async let兩行即可。
但像搜尋欄位那樣 持續流入的值串流,情況就不同了。若要對文字輸入、位置更新、WebSocket 訊息或 ViewModel 狀態變更等不斷延續的事件,加上 debounce、combineLatest 等以時間為基礎的控制,仍然屬於反應式工具的領域。Apple 也透過 AsyncSequence 擴展這個領域,但運算子生態系目前仍以 Combine 較完整。
總結如下。
- 單次非同步工作(例如網路請求)→ async/await
- 持續事件串流+時間控制、UI 綁定 → Combine(或 RxSwift)
兩者與其說是競爭關係,不如說是負責不同領域的工具。
結語
把使用 RxSwift 和 Combine 的原因濃縮成一句話,就是:將各自不同的非同步事件統一成單一串流介面,並在其上以宣告式方式處理組合、時間控制與綁定。
- 將分散在委派、閉包和通知中的事件處理統一成一種方式
- 不使用巢狀回呼來表達多個非同步工作的組合
- 用已驗證的運算子處理 debounce、去除重複和請求取消等時間控制
- MVVM 綁定的實質標準元件
語法初看似乎有很高的進入門檻,但只要掌握「把事件視為值的串流」這個觀點,運算子不過是將陣列的 map、filter沿著時間軸延伸而已。下一篇將深入拆解 Combine 的 Publisher 與 Subscriber 實際如何互相配合運作。

