Combine là framework lập trình phản ứng của Apple, được giới thiệu trong iOS 13. Theo Apple Documentation, 2025, Combine cung cấp API khai báo thống nhất để xử lý các sự kiện bất đồng bộ. Publisher xác định nguồn dữ liệu, và Subscriber đăng ký nhận dữ liệu.
Những điểm chính
Combine là framework Swift khai báo để xử lý các sự kiện bất đồng bộ theo thời gian. Không giống các phương pháp dựa trên callback, Combine cho phép mô tả đường ống xử lý dữ liệu thông qua một chuỗi các toán tử. Framework được tích hợp với Foundation (URLSession, Timer, NotificationCenter) và SwiftUI (ObservableObject, @Published).
Bất kỳ kiểu nào triển khai giao thức Publisher phải xác định Output (kiểu giá trị) và Failure (kiểu lỗi). Publisher không hoạt động cho đến khi được đăng ký — nó chỉ bắt đầu phát sự kiện sau khi gọi subscribe. Đây là ngữ nghĩa lạnh, đặc trưng của các luồng phản ứng, cải thiện hiệu suất.
Subscriber nhận sự kiện thông qua ba phương thức: receive(subscription:) — xác nhận đăng ký với quản lý demand; receive(_:) — nhận giá trị mới; receive(completion:) — kết thúc luồng với thành công hoặc lỗi. Demand xác định số lượng giá trị mà Subscriber sẵn sàng chấp nhận — đây là cơ chế backpressure.
Apple cung cấp nhiều Publisher tích hợp sẵn: Just phát một giá trị duy nhất và kết thúc, Future — kết quả bất đồng bộ với closure, Deferred — tạo Publisher trễ cho đến khi đăng ký. URLSession.dataTaskPublisher chuyển đổi yêu cầu mạng thành luồng phản ứng. Timer.publish tạo bộ đếm thời gian định kỳ. NotificationCenter.default.publisher biến thông báo thành Publisher. Những tích hợp này cho phép đăng ký sự kiện mà không cần cầu nối thủ công.
Trong Combine, Publisher và Subscriber được kết nối thông qua giao thức Subscription. Subscription là đối tượng kết nối kiểm soát luồng dữ liệu. Nhà phát triển không cần tạo kiểu Publisher riêng — Apple cung cấp các kiểu tích hợp sẵn: Just, Future, Deferred, Fail, Empty, cũng như Publisher cho URLSession và Timer.
Subject là Publisher mà có thể gửi giá trị thủ công. PassthroughSubject không lưu trạng thái: người đăng ký chỉ nhận được các sự kiện xảy ra sau khi đăng ký. CurrentValueSubject, tương tự BehaviorRelay trong RxSwift, lưu trữ giá trị hiện tại và chuyển ngay cho người đăng ký mới.
Cơ chế backpressure trong Combine điều chỉnh tốc độ truyền dữ liệu. Subscriber thông báo cho Publisher qua subscription.request(.unlimited) hoặc subscription.request(.max(N)) về số lượng giá trị sẵn sàng xử lý. Điều này ngăn tràn bộ đệm khi tốc độ của nhà sản xuất và người tiêu dùng không đồng đều.
Combine bao gồm hơn 100 toán tử, được chia thành các danh mục: biến đổi (map, tryMap, flatMap), lọc (filter, compactMap, removeDuplicates), kết hợp (combineLatest, merge, zip), quản lý thời gian (debounce, throttle, delay) và xử lý lỗi (catch, replaceError, retry).
Toán tử combineLatest kết hợp các giá trị mới nhất của hai Publisher — mỗi khi một trong hai phát giá trị mới, closure được gọi với cả hai giá trị đã cập nhật. Toán tử này không thể thiếu cho việc xác thực biểu mẫu, nơi cần theo dõi trạng thái của nhiều trường nhập cùng lúc.
Toán tử debounce trì hoãn việc công bố giá trị cho đến khi khoảng thời gian chỉ định trôi qua mà không có sự kiện mới. Điều này rất quan trọng cho các trường tìm kiếm: yêu cầu máy chủ chỉ được gửi sau khi người dùng ngừng nhập trong 300–500 ms, giảm tải 5–10 lần.
Combine cung cấp một số chiến lược xử lý lỗi ở cấp độ toán tử. catch chặn Failure và thay thế Publisher bằng một phương án dự phòng — luồng tiếp tục mà không bị treo. replaceError(with:) cung cấp giá trị mặc định thay vì lỗi. retry(_:) thử lại đăng ký một số lần chỉ định khi thất bại. Quan trọng: kiểu lỗi Never trong chữ ký Publisher đảm bảo luồng sẽ không bao giờ kết thúc với lỗi — điều này cho phép sử dụng Publisher trong SwiftUI mà không cần xử lý Failure.
Ví dụ đầu tiên cho thấy cách sử dụng cơ bản của Just — Publisher phát một giá trị duy nhất và kết thúc:
let publisher = Just("Hello Combine")
publisher
.sink(receiveCompletion: { completion in
print("Hoàn thành: \(completion)")
}, receiveValue: { value in
print("Đã nhận: \(value)")
})
Ví dụ thứ hai minh họa tích hợp Combine với URLSession cho yêu cầu mạng với xử lý kết quả trên luồng chính:
let url = URL(string: "https://api.example.com")!
URLSession.shared
.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Lỗi: \(error)")
}
}, receiveValue: { data in
print("Dữ liệu đã nhận: \(data.count)")
})
Ví dụ thứ ba cho thấy cách sử dụng CurrentValueSubject để quản lý trạng thái:
let counter = CurrentValueSubject<Int, Never>(0)
counter
.sink { value in
print("Bộ đếm: \(value)")
}
counter.send(1) // In ra: Bộ đếm: 1
counter.send(2) // In ra: Bộ đếm: 2
print(counter.value) // In ra: 2
Combine và RxSwift giải quyết cùng một vấn đề — lập trình phản ứng — nhưng với cách tiếp cận khác nhau. Combine là framework gốc của Apple được tích hợp vào hệ thống: không cần cài đặt phụ thuộc, tích hợp với SwiftUI và Foundation. RxSwift là thư viện bên thứ ba hỗ trợ iOS 9+ và có bộ toán tử rộng hơn.
Hiệu suất của Combine cao hơn trong các kịch bản điển hình nhờ triển khai gốc bằng C. RxSwift cung cấp nhiều công cụ gỡ lỗi hơn (RxSwift.Resources.total, Debug), nhưng kiến trúc hướng đối tượng có thể kém hiệu quả hơn trên các luồng dữ liệu lớn. Lựa chọn giữa chúng là sự cân bằng giữa khả năng và tích hợp.
Một khác biệt quan trọng là mô hình backpressure. Trong Combine, Subscriber quản lý demand thông qua Subscription, đưa ra kiểm soát rõ ràng về tốc độ luồng. Trong RxSwift, backpressure không được kiểm soát theo mặc định — Observable phát tất cả các giá trị và Subscriber xử lý chúng khi đến. Đối với luồng dữ liệu lớn, Combine dự đoán được và an toàn về bộ nhớ hơn.
Combine được tích hợp sâu với SwiftUI ở cấp độ ngôn ngữ. @Published tạo Publisher, ObservableObject cung cấp objectWillChange Publisher. SwiftUI View tự động đăng ký các Publisher này và vẽ lại khi có thay đổi. Cho UIKit có các wrapper bên thứ ba như CombineCocoa, nhưng Combine được thiết kế ban đầu cho SwiftUI, do đó tích hợp trong UIKit yêu cầu mã bổ sung. Mặt khác, RxSwift có hệ sinh thái phong phú cho UIKit: RxCocoa cung cấp wrapper phản ứng cho tất cả các phần tử UI.
Combine hỗ trợ tích hợp với Core Data thông qua NSFetchedResultsController Publisher. Publisher phát dữ liệu mới khi có thay đổi trong ngăn xếp Core Data: thêm, xóa, cập nhật bản ghi. Điều này cho phép xây dựng danh sách phản ứng mà không cần gọi reloadData thủ công. Bằng cách kết hợp Combine với NotificationCenter Publisher cho UIApplication.willResignActiveNotification, nhà phát triển tạo một lớp phản ứng cho toàn bộ vòng đời ứng dụng mà không cần ủy quyền và callback. Đối với các lớp ObservableObject, SwiftUI tự động cập nhật View khi các thuộc tính @Published thay đổi — Combine xử lý toàn bộ chuỗi mà không cần một dòng mã thủ công nào. Kiểm thử đơn vị mã Combine được thực hiện thông qua XCTestExpectation và Publisher.sink trong các trường hợp kiểm thử với sự cô lập Scheduler qua ImmediateScheduler.
Khi làm việc với Combine trong các dự án UIKit, nhà phát triển thường đăng ký qua sink và lưu trữ AnyCancellable trong Set. Mô hình điển hình: ViewController tạo view, lưu trữ cancellables, đăng ký Publisher từ ViewModel. Tại deinit, tất cả đăng ký tự động bị hủy thông qua bộ sưu tập Cancellables. Để xử lý UIControl đúng cách trong Combine, có thể sử dụng UIControl.Event Publisher thông qua phần mở rộng publisher(for:).
Gỡ lỗi Combine được thực hiện thông qua toán tử print() hoặc handleEvents(). print(String) ghi nhật ký tất cả các sự kiện: receive subscription, request demand, receive value, receive completion. handleEvents cung cấp kiểm soát chi tiết hơn: có thể chỉ định closure cho từng giai đoạn trong vòng đời Publisher. Để kiểm thử, có CombineExpectations — thư viện cho phép kiểm tra xem Publisher có phát các giá trị mong đợi trong các bài kiểm thử với thời gian ảo và kiểm soát demand hay không.
Combine được sử dụng tích cực trong SwiftUI để quản lý biểu mẫu: mỗi thuộc tính @Published tạo một Publisher, SwiftUI đăng ký nó qua View.body. Bằng cách kết hợp nhiều Publisher qua combineLatest, việc xác thực biểu mẫu trở nên khai báo: mỗi Publisher theo dõi một trường, combineLatest thu thập tất cả giá trị, map xác thực chúng và trả về trạng thái nút Submit. Publishers.Merge kết hợp nhiều Publisher cùng kiểu thành một — hữu ích để thu thập sự kiện từ các điều khiển khác nhau vào một luồng duy nhất.
Để tích hợp Combine với UIKit, hãy sử dụng UIControl Publisher thông qua phần mở rộng: button.publisher(for: .touchUpInside) trả về Publisher phát sự kiện khi nhấn. CombineCocoa là thư viện cộng đồng cung cấp Publisher cho tất cả sự kiện UIControl, UITextView.text, UIScrollView.contentOffset. Khi hủy đăng ký, kho cancellables được dọn trong deinit, đảm bảo không có đăng ký nào sống lâu hơn chủ sở hữu. Đối với UIControl có nhiều trạng thái, hãy sử dụng Publishers.MergeMany để kết hợp các Publisher thành mảng.
Câu hỏi thường gặp
Combine khả dụng từ iOS 13, macOS 10.15, tvOS 13 và watchOS 6. Điều này hạn chế sử dụng trong các dự án yêu cầu hỗ trợ phiên bản cũ. Cho iOS 12 trở xuống, RxSwift hoặc thư viện bên thứ ba được sử dụng.
PassthroughSubject không lưu trạng thái và không phát lại giá trị cuối cùng cho người đăng ký mới — họ chỉ nhận được các sự kiện trong tương lai. CurrentValueSubject lưu trữ giá trị hiện tại và chuyển ngay cho người đăng ký mới. CurrentValueSubject.value cho phép đọc và ghi giá trị hiện tại một cách đồng bộ.
Combine là cốt lõi của SwiftUI: @Published tạo Publisher, ObservableObject sử dụng objectWillChange Publisher. SwiftUI View tự động đăng ký @ObservedObject và @StateObject thông qua đường ống Combine, vẽ lại View khi các thuộc tính thay đổi.
Backpressure là cơ chế kiểm soát tốc độ truyền dữ liệu. Subscriber thông báo cho Subscription biết số lượng giá trị sẵn sàng xử lý (demand). Publisher không thể phát nhiều hơn yêu cầu. Điều này rất quan trọng khi làm việc với các luồng dữ liệu lớn từ socket mạng.
Toán tử catch chặn lỗi và thay thế Publisher bằng phương án dự phòng. replaceError thay thế lỗi bằng giá trị mặc định. retry thử lại đăng ký một số lần chỉ định khi có lỗi. Tất cả các toán tử nên được đặt trước subscribe để tránh treo khi Failure.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm