# 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. - Published: 2026-02-18 - Updated: 2026-04-28 - Author: SharpSkill - Tags: combine, swift, ios, reactive, async - Reading time: 14 min --- 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. > **Tại sao chọn Combine thay vì RxSwift?** > > 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: ```swift // PublisherBasics.swift 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(0) // PassthroughSubject: emits values without storing them // Ideal for one-time events (taps, notifications) let buttonTaps = PassthroughSubject() // Future: emits a single value asynchronously // Wraps an async operation that returns a result let asyncOperation = Future { 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: ```swift // SubscriberBasics.swift import Combine // Variable to store subscriptions // Without this reference, the subscription would be immediately cancelled var cancellables = Set() 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 successfully ``` > **Cẩn trọng với rò rỉ bộ nhớ** > > Luô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: ```swift // TransformOperators.swift import Combine var cancellables = Set() // 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 { // 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: ```swift // FilterOperators.swift import Combine var cancellables = Set() 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() 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 300ms ``` ## 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: ```swift // CombiningPublishers.swift import Combine var cancellables = Set() // Simulate a form with validation let email = CurrentValueSubject("") let password = CurrentValueSubject("") // 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() let secondAPI = PassthroughSubject() 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: ```swift // MergePublishers.swift import Combine var cancellables = Set() // Multiple user notification sources let pushNotifications = PassthroughSubject() let localNotifications = PassthroughSubject() let inAppMessages = PassthroughSubject() // 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: ```swift // ErrorHandling.swift import Combine var cancellables = Set() enum APIError: Error { case networkError case invalidResponse case serverError(Int) } // Simulate an API call that can fail func fetchData() -> AnyPublisher { Fail(error: APIError.networkError) .eraseToAnyPublisher() } // retry: retries N times before propagating the error fetchData() .retry(3) // Try up to 3 times .catch { error -> Just 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) ``` > **Never so với Error** > > 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: ```swift // UserListViewModel.swift 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() 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: ```swift // UserService.swift 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() } } ``` ## 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: ```swift // UserListView.swift 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: ```swift // CancellationPatterns.swift import Combine final class DataManager { // Set of cancellables: automatically cancelled on destruction private var cancellables = Set() // 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: ```swift // SchedulerPatterns.swift import Combine import Foundation var cancellables = Set() func loadAndProcessData() -> AnyPublisher { 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ios/combine-reactive-programming-swift