Combine là framework lập trình phản ứng gốc của Apple, được giới thiệu trong iOS 13, macOS Catalina, tvOS 13 và watchOS 6. Nó cung cấp API Swift khai báo để xử lý các sự kiện bất đồng bộ thông qua mẫu Publisher và Subscriber, thay thế delegate, closure và NotificationCenter bằng một chuỗi thống nhất. Theo Apple, 2025, Combine là nền tảng cho SwiftUI và các kiến trúc iOS hiện đại, hoạt động chặt chẽ với async/await và Structured Concurrency. Framework được thiết kế để kết hợp các hoạt động bất đồng bộ với đảm bảo an toàn luồng.
Những điểm chính
Combine là một framework lập trình phản ứng khai báo được tích hợp trong SDK của Apple. Nó triển khai mẫu Reactive Streams: Publisher tạo ra giá trị, Subscriber tiêu thụ chúng và các toán tử biến đổi luồng giữa chúng. Combine giải quyết vấn đề callback và delegate bằng cách cung cấp một mô hình kết hợp thống nhất cho bất kỳ sự kiện bất đồng bộ nào — từ phản hồi mạng đến thay đổi giao diện người dùng.
Trước Combine, các nhà phát triển iOS sử dụng thư viện bên thứ ba, chủ yếu là RxSwift. Apple đã tạo ra Combine như một giải pháp thay thế gốc với tích hợp sâu vào hệ sinh thái: framework hỗ trợ Objective-C thông qua cầu nối @objc, hoạt động với KVO (Key-Value Observing qua NSObject.keyValuePublisher) và NotificationCenter, đồng thời là nền tảng cho SwiftUI. Tất cả các thành phần UIKit được xuất bản trong SwiftUI đều sử dụng Combine bên trong để cập nhật chế độ xem.
Combine được thiết kế có tính đến Swift Concurrency: bắt đầu từ iOS 15, Publisher có thể được chuyển đổi thành AsyncSequence thông qua .values và sử dụng trong vòng lặp for-await-in. Chuyển đổi ngược từ hàm async thành Publisher được thực hiện thông qua Future. Theo Apple WWDC 2024, Combine vẫn là framework được khuyến nghị để xử lý dữ liệu luồng trong các ứng dụng UIKit, mặc dù đã có async/await cho các cuộc gọi bất đồng bộ đơn lẻ.
Combine dựa trên ba giao thức: Publisher (phát ra giá trị kiểu Output, có thể thất bại với lỗi kiểu Failure), Subscriber (nhận giá trị, quản lý Demand — số lượng phần tử được yêu cầu), Subscription (đại diện cho kết nối Publisher-Subscriber với khả năng hủy). Kênh dữ liệu được khởi tạo khi đăng ký và kết thúc khi hủy, hoàn thành hoặc lỗi. Demand là một khái niệm độc đáo của Combine: Subscriber thông báo cho Publisher biết nó sẵn sàng xử lý bao nhiêu phần tử, triển khai backpressure ở cấp độ giao thức.
Publisher là một giao thức với hai kiểu liên kết: Output (kiểu giá trị được phát ra) và Failure (kiểu lỗi triển khai Error). Nếu luồng không thể thất bại, Failure được chỉ định là Never — điều này đảm bảo cho Subscriber rằng onReceive sẽ chỉ được gọi với Output. Các Publisher tích hợp bao gồm Just (giá trị đơn), Sequence (mảng), URLSession.DataTaskPublisher (yêu cầu mạng), NotificationCenter.Publisher và property wrapper @Published.
import Combine
// Tạo Publisher từ một chuỗi
let publisher = [1, 2, 3, 4, 5].publisher
// Tạo Subscriber với xử lý giá trị
class PrintSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("Hoàn thành")
}
}
publisher.subscribe(PrintSubscriber())
Subscription là một giao thức đại diện cho kết nối hoạt động giữa Publisher và Subscriber. Subscriber nhận Subscription trong phương thức receive(subscription:) và gọi request(_:) để chỉ định Demand: .unlimited (tất cả giá trị), .max(N) (số lượng giới hạn) hoặc .none (tạm dừng). Demand có thể thay đổi động — Subscriber có thể tăng hoặc giảm số lượng phần tử được yêu cầu trong quá trình nhận dữ liệu. Điều này cung cấp backpressure mà không cần bộ đệm ở phía Publisher.
Subject là một kiểu kết hợp Publisher và Subscriber. Subject có thể được sử dụng như Publisher (người đăng ký đăng ký vào nó) và đồng thời như Subscriber (các giá trị được gửi vào nó). Combine cung cấp hai loại Subject: PassthroughSubject (không lưu trạng thái, chỉ chuyển tiếp giá trị mới) và CurrentValueSubject (lưu giá trị hiện tại và chuyển nó cho người đăng ký mới). Subject cần thiết để tích hợp mã mệnh lệnh vào chuỗi phản ứng của Combine.
let subject = PassthroughSubject<String, Never>()
// Đăng ký như Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Gửi giá trị như Subscriber
subject.send("hello") // In "HELLO"
subject.send("world") // In "WORLD"
CurrentValueSubject khác với PassthroughSubject ở chỗ có giá trị ban đầu và thuộc tính value: người đăng ký ngay lập tức nhận được giá trị hiện tại khi đăng ký, sau đó là tất cả các cập nhật tiếp theo. CurrentValueSubject.value có thể đọc và ghi — thay đổi value tự động gửi giá trị mới đến tất cả người đăng ký. Điều này làm cho CurrentValueSubject trở thành lựa chọn lý tưởng để đại diện trạng thái trong kiến trúc MVVM: ViewModel xuất bản CurrentValueSubject, View đăng ký thay đổi thông qua sink.
Cả hai Subject có thể kết thúc luồng bằng cách gọi send(completion: .finished) hoặc send(completion: .failure(error)). Sau khi kết thúc, Subject ngừng chấp nhận và phát ra sự kiện. Đối với các luồng dài hạn không nên kết thúc (ví dụ: sự kiện UI), nên sử dụng PassthroughSubject với Never Failure để tránh vô tình gọi send(completion:).
Toán tử Combine là các phương thức của Publisher trả về một Publisher mới. Mỗi toán tử tạo một đối tượng mới đăng ký vào Publisher upstream và phát ra các giá trị đã biến đổi downstream. Vì Publisher là kiểu chung, các toán tử duy trì kiểu nghiêm ngặt: map biến đổi Output<A> thành Output<B>, tryMap thêm khả năng lỗi. Combine chứa khoảng 100 toán tử tích hợp.
| Danh mục | Toán tử | Mục đích |
|---|---|---|
| Biến đổi | map / tryMap / flatMap | Biến đổi giá trị hoặc luồng |
| Lọc | filter / compactMap / removeDuplicates | Chọn hoặc làm sạch giá trị |
| Kết hợp | combineLatest / zip / merge | Kết hợp nhiều Publisher |
| Kiểm soát thời gian | debounce / throttle / delay | Trì hoãn và giãn cách sự kiện |
| Xử lý lỗi | catch / retry / replaceError | Phục hồi sau Failure |
| Quản lý Demand | buffer / collect | Nhóm hoặc đệm |
flatMap trong Combine có sự khác biệt quan trọng so với phiên bản RxSwift: nó chấp nhận một closure trả về Publisher với cùng kiểu Failure và làm phẳng Publisher lồng nhau vào luồng chính. flatMap với maxPublishers: .max(1) hoạt động như switchMap — nó hủy Publisher lồng nhau trước đó khi một giá trị mới đến. Điều này rất quan trọng cho các kịch bản tìm kiếm: khi một ký tự mới được nhập, yêu cầu HTTP trước đó tự động bị hủy.
// Tìm kiếm debounce với hủy yêu cầu trước đó
searchTextField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap(maxPublishers: .max(1)) { query in
apiService.searchPublisher(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink { results in
self.tableView.reloadData()
}
.store(in: &cancellables)
Toán tử kết hợp — combineLatest và zip — hoạt động tương tự RxSwift: combineLatest phát ra một bộ các giá trị mới nhất từ tất cả Publisher khi bất kỳ thay đổi nào; zip ghép cặp giá trị theo chỉ mục. merge kết hợp các Publisher cùng loại thành một luồng, thứ tự không được đảm bảo. Combine cũng có select — một toán tử hiếm chọn Publisher hoàn thành đầu tiên trong số nhiều, và share — phát đa hướng một luồng đến nhiều người đăng ký mà không thực thi lại.
Scheduler trong Combine là một giao thức xác định ngữ cảnh thực thi cho các toán tử. Không giống như RxSwift với hơn 5 Scheduler tích hợp, Combine sử dụng cơ chế Apple hiện có: DispatchQueue, RunLoop và OperationQueue. Mỗi loại này tuân thủ giao thức Scheduler, cho phép chuyển chúng trực tiếp đến receive(on:) và subscribe(on:) mà không cần bộ điều hợp bổ sung.
receive(on:) chuyển downstream sang Scheduler được chỉ định — tương đương với observeOn trong RxSwift. Tất cả các toán tử sau receive(on:) thực thi trên Scheduler được chỉ định. subscribe(on:) chuyển upstream — ảnh hưởng đến việc thực thi Publisher. Một mẫu điển hình: subscribe(on: DispatchQueue.global()) cho công việc nền và receive(on: DispatchQueue.main) cho cập nhật UI. Trong SwiftUI, khi sử dụng .onReceive, không cần liên kết tích hợp đến luồng chính, nhưng đối với sink, receive(on:).main rõ ràng được khuyến nghị.
// Tải nền + UI trên luồng chính
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) },
receiveValue: { self.nameLabel.text = $0.name })
.store(in: &cancellables)
RunLoop.main là một thay thế cho DispatchQueue.main cho các hoạt động UI. Sự khác biệt là RunLoop.main gắn với vòng lặp sự kiện ứng dụng hiện tại, trong khi DispatchQueue.main gắn với hàng đợi toàn cục của luồng chính. Đối với UIKit, DispatchQueue.main được khuyến nghị; đối với SwiftUI — RunLoop.main. ImmediateWhenScheduler thực thi các hoạt động đồng bộ trên luồng hiện tại — được sử dụng mặc định cho các bài kiểm tra và Publisher đơn giản.
ObservableObject là một giao thức SwiftUI cho các đối tượng xuất bản thay đổi. Một lớp triển khai ObservableObject có thể sử dụng property wrapper @Published cho các thuộc tính mà sự thay đổi của chúng tự động thông báo cho SwiftUI về nhu cầu vẽ lại. Bên trong, @Published tạo một Publisher thông báo cho Publisher objectWillChange khi wrappedValue thay đổi. SwiftUI đăng ký objectWillChange thông qua @StateObject, @ObservedObject hoặc @EnvironmentObject.
@Published là cách phổ biến nhất để tích hợp Combine vào SwiftUI. Khi giá trị của thuộc tính @Published thay đổi, SwiftUI cập nhật tất cả Views sử dụng đối tượng đó. @StateObject tạo một phiên bản ObservableObject và đăng ký các thay đổi của nó. Một View được tạo với @StateObject tự động vẽ lại khi các thuộc tính @Published thay đổi. Nếu đối tượng cần được chia sẻ giữa nhiều Views, @ObservedObject hoặc @EnvironmentObject được sử dụng.
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] newName in
AnalyticsService.logNameChange(newName)
}
.store(in: &cancellables)
}
}
struct UserView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
AnyCancellable là một trình bao bọc xóa kiểu cho Cancellable lưu trữ mã thông báo hủy đăng ký. Set<AnyCancellable> quản lý vòng đời của các đăng ký: khi chủ sở hữu bị hủy khởi tạo, tất cả Cancellable tự động bị hủy. Trong các dự án SwiftUI, Set<AnyCancellable> được khai báo trong lớp ObservableObject và các đăng ký được thêm qua .store(in: &cancellables). Đối với UIKit, các cơ chế tương tự được sử dụng với lưu trữ trong UIViewController qua &cancellables hoặc gọi thủ công cancel().
Combine và RxSwift giải quyết các tác vụ lập trình phản ứng giống nhau nhưng có sự khác biệt về kiến trúc cơ bản. Combine là một phần của SDK Apple với khả năng tương thích ngược đến iOS 13, RxSwift là thư viện bên thứ ba hỗ trợ iOS 8+. Combine sử dụng kiểu lỗi nghiêm ngặt thông qua Failure chung, RxSwift sử dụng một kiểu Error duy nhất. Combine được tích hợp với SwiftUI ở cấp nền tảng, RxSwift yêu cầu RxCocoa cho các mở rộng UI.
Sự lựa chọn giữa Combine và RxSwift phụ thuộc vào yêu cầu dự án. Nếu phiên bản iOS tối thiểu >= 13 và dự án sử dụng SwiftUI — Combine là lựa chọn tự nhiên nhờ tích hợp sẵn và không có phụ thuộc bổ sung. Nếu dự án hỗ trợ iOS 11-12, chứa cơ sở mã RxSwift hiện có hoặc yêu cầu các toán tử cụ thể chỉ có trong RxSwift (ví dụ: Observable.from(path:)), — RxSwift vẫn là giải pháp hợp lệ.
| Đặc điểm | Combine | RxSwift |
|---|---|---|
| Nhà phát triển | Apple (tích hợp trong SDK) | ReactiveX (cộng đồng) |
| Phiên bản iOS | iOS 13+ | iOS 8+ |
| Kiểu lỗi | Failure chung (Never cho UI) | Error (bất kỳ) |
| Tích hợp UI | @Published + SwiftUI | RxCocoa + UIKit |
| Toán tử | ~100 tích hợp | 400+ toán tử |
| Swift Concurrency | Qua .values (chuỗi bất đồng bộ) | Qua thư viện cầu nối |
Câu hỏi thường gặp
PassthroughSubject không lưu trạng thái — người đăng ký chỉ nhận được sự kiện được gửi sau khi đăng ký. CurrentValueSubject lưu giá trị hiện tại và chuyển nó cho mỗi người đăng ký mới ngay khi đăng ký. CurrentValueSubject phù hợp để đại diện trạng thái (ví dụ: isLoggedIn).
Một đăng ký trả về AnyCancellable, bị hủy khi gọi cancel() hoặc khi hủy khởi tạo. Để quản lý nhóm, sử dụng Set<AnyCancellable> — tất cả đăng ký bị hủy khi tập hợp bị xóa. Điều này tương tự như DisposeBag trong RxSwift.
Có, Combine vẫn phù hợp cho dữ liệu luồng: sự kiện UI, debounce, combineLatest, WebSocket. async/await thuận tiện cho các yêu cầu đơn lẻ, Combine — cho các luồng liên tục hoặc nhiều luồng. Cả hai framework bổ sung cho nhau — Publisher có thể được chuyển đổi thành AsyncSequence.
UIKit không có Publisher tích hợp, nhưng Apple cung cấp các mở rộng: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Cho các sự kiện UI tùy chỉnh, PassthroughSubject hoặc @IBAction được bọc trong Publisher qua Future hoặc Subject được sử dụng.
Backpressure là cơ chế kiểm soát tốc độ luồng: Subscriber thông báo cho Publisher qua Demand biết nó sẵn sàng xử lý bao nhiêu phần tử. Nếu Demand = .max(1), Publisher đợi yêu cầu trước khi gửi giá trị tiếp theo. Điều này ngăn tràn bộ đệm khi tốc độ nhà sản xuất và người tiêu dùng không khớp.
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