iOS 工程

為什麼在 iOS 使用 RxSwift·Combine?反應式程式設計解決的問題

學習 iOS 開發時,到了某個階段一定會遇到 RxSwift 或 Combine 這道牆。它們幾乎必定出現在徵才公告中,但實際打開程式碼,看到 map、flatMap、sink 一路串接的陌生語法,很容易感到不知所措。「明明用閉包和委派就能正常運作,真的有必要學這個嗎?」

閱讀 5 分鐘
為什麼在 iOS 使用 RxSwift·Combine?反應式程式設計解決的問題 封面圖

學習 iOS 開發時,到了某個階段一定會遇到 RxSwift 或 Combine 這道牆。它們幾乎必定出現在徵才公告中,但打開程式碼後,看到 mapflatMapsink一路串接的陌生語法,很容易感到不知所措。「明明用閉包和委派就能正常運作,真的有必要學這個嗎?」有這樣的疑問很自然。

今天先不談語法,從 為什麼會需要這類工具開始整理。理解原因後,語法自然就能跟上。


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 的 @PublishedObservableObject。但 UIKit 綁定支援比 RxCocoa 較少。

新專案通常以 Combine 作為穩妥的預設選擇;加入既有程式碼庫已使用 RxSwift 的團隊時,則常會學習 RxSwift。

RxSwift 和 Combine 是做同一件事的兩個工具箱,選擇取決於團隊情況
RxSwift 和 Combine 是做同一件事的兩個工具箱,選擇取決於團隊情況

有了 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 綁定的實質標準元件

語法初看似乎有很高的進入門檻,但只要掌握「把事件視為值的串流」這個觀點,運算子不過是將陣列的 mapfilter沿著時間軸延伸而已。下一篇將深入拆解 Combine 的 Publisher 與 Subscriber 實際如何互相配合運作。

推薦閱讀