Combine — khái niệm chính, Publisher và lập trình phản ứng

Tác giả: IT Sectr Đã đăng: 2026-05-03 Thời gian đọc: 11 phút

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à framework phản ứng gốc của Apple với các kiểu Publisher, Subscriber, Subject và kiểu lỗi thông qua Failure
  • Publisher phát ra sự kiện theo thời gian: giá trị Output, có thể hoàn thành thành công hoặc lỗi Failure
  • Subscriber nhận sự kiện từ Publisher và có thể yêu cầu số lượng mục cụ thể thông qua Demand
  • Toán tử map, flatMap, filter, combineLatest, zip, debounce biến đổi luồng mà không có điều kiện cạnh tranh
  • SwiftUI sử dụng Combine thông qua @Published, ObservableObject và @StateObject để cập nhật giao diện phản ứng

Combine là gì?

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ẻ.

Khái niệm cơ bản của Combine

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 và Subscriber: Kiến trúc luồng phản ứng

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.

swift
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 và Demand

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 và CurrentValueSubject

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.

swift
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ử biến đổi và kết hợp

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ụcToán tửMục đích
Biến đổimap / tryMap / flatMapBiến đổi giá trị hoặc luồng
Lọcfilter / compactMap / removeDuplicatesChọn hoặc làm sạch giá trị
Kết hợpcombineLatest / zip / mergeKết hợp nhiều Publisher
Kiểm soát thời giandebounce / throttle / delayTrì hoãn và giãn cách sự kiện
Xử lý lỗicatch / retry / replaceErrorPhục hồi sau Failure
Quản lý Demandbuffer / collectNhó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.

swift
// 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 và quản lý luồng

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ị.

swift
// 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.

Combine và SwiftUI: Tích hợp qua ObservableObject

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 và @StateObject

@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.

swift
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 vs RxSwift

CombineRxSwift 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ểmCombineRxSwift
Nhà phát triểnApple (tích hợp trong SDK)ReactiveX (cộng đồng)
Phiên bản iOSiOS 13+iOS 8+
Kiểu lỗiFailure chung (Never cho UI)Error (bất kỳ)
Tích hợp UI@Published + SwiftUIRxCocoa + UIKit
Toán tử~100 tích hợp400+ toán tử
Swift ConcurrencyQua .values (chuỗi bất đồng bộ)Qua thư viện cầu nối

Câu hỏi thường gặp

Sự khác biệt giữa PassthroughSubject và CurrentValueSubject là gì?

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).

Làm thế nào để hủy đăng ký trong Combine?

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.

Tôi có cần học Combine sau async/await không?

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.

Làm thế nào để sử dụng Combine với UIKit?

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 trong Combine là gì?

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

  • Combine là framework phản ứng gốc của Apple cho iOS 13+ với mẫu Publisher-Subscriber và kiểu lỗi nghiêm ngặt
  • PublisherSubscriber tạo thành kênh giao tiếp với quản lý Demand cho backpressure
  • Subject (Passthrough và CurrentValue) tích hợp mã mệnh lệnh vào chuỗi phản ứng
  • Toán tử map, flatMap, combineLatest, debounce, catch cung cấp xử lý luồng khai báo
  • Scheduler qua DispatchQueue và RunLoop quản lý luồng thực thi mà không chặn UI
  • SwiftUI sử dụng Combine qua @Published, ObservableObject và @StateObject cho Views phản ứng
  • RxSwift vẫn là giải pháp thay thế cho dự án với iOS 8+, Combine là lựa chọn cho dự án mới trên iOS 13+

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.

Thảo luận dự án

Đọc thêm