Combine Framework: Lập Trình Phản Ứng Trong Swift
Làm chủ Combine để xử lý các luồng dữ liệu bất đồng bộ trong Swift: Publishers, Subscribers, Operators và các mẫu nâng cao cho ứng dụng iOS.

Lập trình phản ứng thay đổi cách các sự kiện bất đồng bộ và luồng dữ liệu được xử lý trong ứng dụng iOS. Combine, framework gốc của Apple, mang đến cách tiếp cận khai báo và an toàn về kiểu để điều phối các pipeline dữ liệu phức tạp. Hướng dẫn này dẫn dắt từ các khái niệm nền tảng đến những mẫu sẵn sàng cho production.
Combine được tích hợp sẵn trong iOS 13+, cho hiệu năng tốt hơn nhờ tối ưu của Apple và tích hợp mượt mà với SwiftUI. Không phải quản lý phụ thuộc bên ngoài.
Khái niệm cốt lõi của Combine
Combine được xây dựng trên ba khái niệm chính: Publishers phát ra giá trị, Subscribers nhận chúng và Operators biến đổi dữ liệu giữa hai bên. Kiến trúc này cho phép xây dựng các pipeline dữ liệu phản ứng và có khả năng kết hợp.
Publisher: nguồn dữ liệu
Publisher là một kiểu có thể phát ra một chuỗi giá trị theo thời gian. Mỗi Publisher khai báo hai kiểu liên kết: kiểu giá trị phát ra (Output) và kiểu lỗi có thể xảy ra (Failure). Đây là cách tạo các loại Publisher khác nhau:
import Combine
// Just: emits a single value then completes
// Useful for converting a simple value to a Publisher
let singleValue = Just("Hello Combine")
// CurrentValueSubject: stores and emits the current value
// Perfect for representing state that changes over time
let counter = CurrentValueSubject<Int, Never>(0)
// PassthroughSubject: emits values without storing them
// Ideal for one-time events (taps, notifications)
let buttonTaps = PassthroughSubject<Void, Never>()
// Future: emits a single value asynchronously
// Wraps an async operation that returns a result
let asyncOperation = Future<String, Error> { promise in
// Simulate a network call
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
promise(.success("Data loaded"))
}
}Kiểu Never cho lỗi nghĩa là Publisher không bao giờ có thể thất bại. Đây là sự đảm bảo tại thời điểm biên dịch giúp đơn giản hóa mã xử lý lỗi.
Subscriber: nhận giá trị
Một Subscriber đăng ký vào Publisher để nhận giá trị của nó. Phương thức sink là cách phổ biến nhất để tạo Subscriber. Nó nhận hai closure: một cho lỗi hoặc khi hoàn tất và một cho mỗi giá trị nhận được:
import Combine
// Variable to store subscriptions
// Without this reference, the subscription would be immediately cancelled
var cancellables = Set<AnyCancellable>()
let publisher = ["Swift", "Combine", "iOS"].publisher
// sink() creates a Subscriber that receives values
publisher
.sink(
// Called when the Publisher completes or fails
receiveCompletion: { completion in
switch completion {
case .finished:
print("✅ Completed successfully")
case .failure(let error):
print("❌ Error: \(error)")
}
},
// Called for each emitted value
receiveValue: { value in
print("Received: \(value)")
}
)
// store() keeps a reference to the subscription
.store(in: &cancellables)
// Output:
// Received: Swift
// Received: Combine
// Received: iOS
// ✅ Completed successfullyLuôn lưu giữ AnyCancellable mà sink() trả về. Không có tham chiếu, đăng ký sẽ bị hủy tự động và không nhận được giá trị nào.
Biến đổi dữ liệu với Operators
Operators là trái tim của Combine. Chúng cho phép biến đổi, lọc và kết hợp các luồng dữ liệu một cách khai báo. Mỗi Operator trả về một Publisher mới, cho phép xâu chuỗi chúng.
Các Operators biến đổi thiết yếu
Operators biến đổi điều chỉnh từng giá trị phát ra. map biến đổi giá trị, flatMap làm phẳng các Publisher lồng nhau, và compactMap lọc ra giá trị nil:
import Combine
var cancellables = Set<AnyCancellable>()
// map: transforms each value
// Equivalent to map on arrays
[1, 2, 3, 4, 5].publisher
.map { $0 * 2 } // Multiply each number by 2
.sink { print("Doubled: \($0)") }
.store(in: &cancellables)
// Output: 2, 4, 6, 8, 10
// compactMap: transforms AND filters out nil
// Useful for optional conversions
["1", "two", "3", "four", "5"].publisher
.compactMap { Int($0) } // Convert to Int, ignore failures
.sink { print("Valid number: \($0)") }
.store(in: &cancellables)
// Output: 1, 3, 5
// flatMap: flattens nested Publishers
// Essential for chaining async operations
struct User { let id: Int; let name: String }
func fetchUser(id: Int) -> AnyPublisher<User, Never> {
// Simulate an API call
Just(User(id: id, name: "User \(id)"))
.delay(for: .milliseconds(100), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
[1, 2, 3].publisher
.flatMap { id in fetchUser(id: id) } // Each ID becomes an API call
.sink { user in print("User: \(user.name)") }
.store(in: &cancellables)Các Operators lọc
Operators lọc kiểm soát những giá trị nào đi qua pipeline. Chúng quan trọng để tránh xử lý không cần thiết và tối ưu hiệu năng:
import Combine
var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5].publisher
// filter: keeps only values that satisfy the condition
numbers
.filter { $0 > 2 } // Keep only numbers > 2
.sink { print("Filtered: \($0)") }
.store(in: &cancellables)
// Output: 3, 3, 3, 4, 5, 5
// removeDuplicates: removes consecutive identical values
numbers
.removeDuplicates() // Eliminate consecutive duplicates
.sink { print("Without duplicates: \($0)") }
.store(in: &cancellables)
// Output: 1, 2, 3, 4, 5
// debounce: waits for a pause before emitting
// Perfect for real-time search
let searchText = PassthroughSubject<String, Never>()
searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates() // Ignore if text hasn't changed
.sink { query in
print("Search: \(query)")
// Launch API call here
}
.store(in: &cancellables)
// Simulate rapid typing
searchText.send("S")
searchText.send("Sw")
searchText.send("Swi")
searchText.send("Swift") // Only "Swift" is emitted after 300msSẵn sàng chinh phục phỏng vấn iOS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Kết hợp nhiều Publisher
Ứng dụng thực tế thường cần kết hợp nhiều nguồn dữ liệu. Combine cung cấp nhiều Operators để điều phối các luồng đa nguồn này.
CombineLatest và Zip
combineLatest phát ra mỗi khi bất kỳ Publisher nào phát ra, kết hợp với giá trị mới nhất từ các Publisher khác. zip đợi tất cả các Publisher phát ra trước khi kết hợp:
import Combine
var cancellables = Set<AnyCancellable>()
// Simulate a form with validation
let email = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("")
// combineLatest: combines the latest values from each Publisher
// Emits on every change from either source
Publishers.CombineLatest(email, password)
.map { email, password in
// Validate that email contains @ and password > 6 chars
let isEmailValid = email.contains("@")
let isPasswordValid = password.count >= 6
return isEmailValid && isPasswordValid
}
.sink { isFormValid in
print("Form valid: \(isFormValid)")
}
.store(in: &cancellables)
email.send("user@example.com") // false (password empty)
password.send("123456") // true (both are valid)
// zip: waits for one value from each Publisher before emitting
// Useful for synchronizing parallel operations
let firstAPI = PassthroughSubject<String, Never>()
let secondAPI = PassthroughSubject<Int, Never>()
Publishers.Zip(firstAPI, secondAPI)
.sink { stringValue, intValue in
print("Received pair: \(stringValue), \(intValue)")
}
.store(in: &cancellables)
firstAPI.send("Hello") // No emission, waiting for secondAPI
secondAPI.send(42) // Emits: ("Hello", 42)
firstAPI.send("World") // No emission, waiting for secondAPI
secondAPI.send(100) // Emits: ("World", 100)Merge để hợp nhất các luồng
merge gộp nhiều Publisher cùng kiểu thành một luồng duy nhất. Các giá trị đến theo thứ tự phát ra, bất kể Publisher nào đã gửi:
import Combine
var cancellables = Set<AnyCancellable>()
// Multiple user notification sources
let pushNotifications = PassthroughSubject<String, Never>()
let localNotifications = PassthroughSubject<String, Never>()
let inAppMessages = PassthroughSubject<String, Never>()
// Merge unifies all streams into one
Publishers.Merge3(pushNotifications, localNotifications, inAppMessages)
.sink { message in
// Handle all notifications the same way
print("📬 Notification: \(message)")
}
.store(in: &cancellables)
pushNotifications.send("New message") // 📬 Notification: New message
localNotifications.send("Reminder: meeting") // 📬 Notification: Reminder: meeting
inAppMessages.send("Welcome!") // 📬 Notification: Welcome!Xử lý lỗi trong Combine
Xử lý lỗi được tích hợp vào lõi của Combine. Kiểu Failure của Publisher cho phép trình biên dịch xác minh rằng mọi lỗi đều được xử lý.
Chiến lược phục hồi
Combine cung cấp nhiều Operators để xử lý lỗi: catch để thay thế bằng Publisher khác, retry để thử lại, và replaceError để dùng giá trị mặc định:
import Combine
var cancellables = Set<AnyCancellable>()
enum APIError: Error {
case networkError
case invalidResponse
case serverError(Int)
}
// Simulate an API call that can fail
func fetchData() -> AnyPublisher<String, APIError> {
Fail(error: APIError.networkError)
.eraseToAnyPublisher()
}
// retry: retries N times before propagating the error
fetchData()
.retry(3) // Try up to 3 times
.catch { error -> Just<String> in
// catch: replaces the error with a fallback Publisher
print("Error after 3 attempts: \(error)")
return Just("Cached data") // Fallback value
}
.sink(
receiveCompletion: { _ in },
receiveValue: { print("Result: \($0)") }
)
.store(in: &cancellables)
// replaceError: replaces any error with a fixed value
// Simpler than catch when only a default value is needed
fetchData()
.replaceError(with: "Error - default value")
.sink { print("With fallback: \($0)") }
.store(in: &cancellables)Dùng setFailureType(to:) để chuyển một Publisher Never thành Publisher có thể thất bại, và replaceError(with:) hoặc catch để làm chiều ngược lại.
Mẫu MVVM với Combine
Combine tích hợp tự nhiên với mẫu MVVM (Model-View-ViewModel). ViewModel cung cấp các Publisher để View quan sát, tạo binding phản ứng giữa dữ liệu và giao diện.
ViewModel phản ứng đầy đủ
Đây là ví dụ ViewModel cho một danh sách người dùng có tìm kiếm, tải dữ liệu và xử lý lỗi:
import Combine
import Foundation
// Data model
struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
}
// ViewModel with reactive state
final class UserListViewModel: ObservableObject {
// MARK: - Published Properties (observed by SwiftUI)
@Published var users: [User] = [] // User list
@Published var searchQuery: String = "" // Search text
@Published var isLoading: Bool = false // Loading state
@Published var errorMessage: String? // Optional error message
// MARK: - Private Properties
private var cancellables = Set<AnyCancellable>()
private let userService: UserServiceProtocol
// MARK: - Computed Properties
// Filters users based on search query
var filteredUsers: [User] {
guard !searchQuery.isEmpty else { return users }
return users.filter {
$0.name.localizedCaseInsensitiveContains(searchQuery)
}
}
// MARK: - Initialization
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
setupBindings()
}
// MARK: - Private Methods
private func setupBindings() {
// Observe searchQuery changes
// debounce prevents too frequent calls
$searchQuery
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] query in
// Server-side search logic if needed
print("Search updated: \(query)")
}
.store(in: &cancellables)
}
// MARK: - Public Methods
func loadUsers() {
isLoading = true
errorMessage = nil
userService.fetchUsers()
.receive(on: DispatchQueue.main) // Ensure UI updates on main thread
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] users in
self?.users = users
}
)
.store(in: &cancellables)
}
}Service với Combine và URLSession
URLSession tích hợp Combine một cách tự nhiên qua dataTaskPublisher. Đây là cách tạo một service mạng có thể tái sử dụng:
import Combine
import Foundation
protocol UserServiceProtocol {
func fetchUsers() -> AnyPublisher<[User], Error>
}
final class UserService: UserServiceProtocol {
private let baseURL = URL(string: "https://api.example.com")!
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
func fetchUsers() -> AnyPublisher<[User], Error> {
let url = baseURL.appendingPathComponent("users")
return session.dataTaskPublisher(for: url)
// Check HTTP status code
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard 200..<300 ~= httpResponse.statusCode else {
throw URLError(.badServerResponse)
}
return data
}
// Decode JSON to Swift model
.decode(type: [User].self, decoder: decoder)
// Erase concrete type to return AnyPublisher
.eraseToAnyPublisher()
}
}Sẵn sàng chinh phục phỏng vấn iOS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Tích hợp với SwiftUI
Combine và SwiftUI tạo nên một bộ đôi mạnh mẽ. Các thuộc tính @Published của một ObservableObject tự động kích hoạt cập nhật view.
View SwiftUI với ViewModel Combine
Đây là cách kết nối ViewModel với một view SwiftUI:
import SwiftUI
struct UserListView: View {
// StateObject: creates and owns the ViewModel
@StateObject private var viewModel = UserListViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
// Centered loading indicator
ProgressView("Loading...")
} else if let error = viewModel.errorMessage {
// Error view with retry button
VStack(spacing: 16) {
Text("Error: \(error)")
.foregroundStyle(.red)
Button("Retry") {
viewModel.loadUsers()
}
}
} else {
// User list
List(viewModel.filteredUsers) { user in
UserRowView(user: user)
}
}
}
.navigationTitle("Users")
.searchable(text: $viewModel.searchQuery) // Direct binding
.onAppear {
viewModel.loadUsers() // Load on first appearance
}
}
}
}
struct UserRowView: View {
let user: User
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}Mẫu nâng cao
Hủy bỏ và dọn dẹp tự động
Quản lý vòng đời đăng ký rất quan trọng để tránh rò rỉ bộ nhớ. Mẫu cancellables với AnyCancellable đảm bảo dọn dẹp tự động:
import Combine
final class DataManager {
// Set of cancellables: automatically cancelled on destruction
private var cancellables = Set<AnyCancellable>()
// Individual cancellable for fine-grained control
private var currentRequest: AnyCancellable?
func startPolling() {
// Timer that emits every 5 seconds
Timer.publish(every: 5, on: .main, in: .common)
.autoconnect() // Starts automatically
.sink { [weak self] _ in
self?.fetchLatestData()
}
.store(in: &cancellables)
}
func fetchLatestData() {
// Cancel the previous request if it exists
currentRequest?.cancel()
currentRequest = URLSession.shared
.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map(\.data)
.decode(type: [String].self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.sink { data in
print("Data received: \(data)")
}
}
deinit {
// All cancellables are automatically cancelled
print("DataManager destroyed, subscriptions cancelled")
}
}Schedulers cho threading
Schedulers kiểm soát thread mà các thao tác chạy trên đó. Dùng subscribe(on:) cho công việc nền và receive(on:) cho cập nhật UI:
import Combine
import Foundation
var cancellables = Set<AnyCancellable>()
func loadAndProcessData() -> AnyPublisher<ProcessedData, Error> {
URLSession.shared.dataTaskPublisher(for: apiURL)
// Perform parsing on a background thread
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.map(\.data)
.decode(type: RawData.self, decoder: JSONDecoder())
// Heavy processing on background thread
.map { rawData in
// This expensive operation runs in the background
processData(rawData)
}
// Return to main thread for UI
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}Kết luận
Combine cung cấp cách tiếp cận mạnh mẽ và khai báo để xử lý các luồng dữ liệu bất đồng bộ trong ứng dụng iOS. Các điểm chính:
✅ Publishers phát ra giá trị theo thời gian
✅ Subscribers nhận và xử lý các giá trị đó
✅ Operators biến đổi và kết hợp các luồng
✅ AnyCancellable quản lý vòng đời đăng ký
✅ @Published với SwiftUI tạo binding phản ứng tự động
✅ Schedulers kiểm soát threading để có hiệu năng tối ưu
Làm chủ Combine giúp xây dựng các ứng dụng iOS bền vững, dễ bảo trì và phản ứng. Sự tích hợp tự nhiên với SwiftUI biến nó thành công cụ thiết yếu cho phát triển iOS hiện đại.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Combine vs async/await trong Swift: Mẫu Hình Di Cư Tiến Bộ
Hướng dẫn đầy đủ về di cư từ Combine sang async/await trong Swift: chiến lược tiến bộ, mẫu hình bắc cầu và sự cùng tồn tại của các mô hình trong codebase iOS.

Câu hỏi phỏng vấn về khả năng tiếp cận iOS năm 2026: VoiceOver và Dynamic Type
Chuẩn bị phỏng vấn iOS với những câu hỏi then chốt về khả năng tiếp cận: VoiceOver, Dynamic Type, các trait ngữ nghĩa và kiểm thử.

Swift Macros: ví dụ thực tế về metaprogramming
Hướng dẫn đầy đủ về Swift Macros: tạo macro freestanding và attached, thao tác AST với swift-syntax, kèm ví dụ thực tế giúp loại bỏ mã lặp.