Combine — ключови понятия, Publisher и реактивно програмиране

Автор: IT Sectr Публикувано: 2026-05-03 Време за четене: 11 мин

Combine — е native реактивна програмен框架 на Apple, представен в iOS 13, macOS Catalina, tvOS 13 и watchOS 6. Той предоставя декларативен Swift API за обработка на асинхронни събития чрез модела Publisher и Subscriber, заменяйки делегатите, затварянията и NotificationCenter с единна верига. Според Apple, 2025, Combine е основата за SwiftUI и съвременните iOS архитектури, работейки в тясна връзка с async/await и Structured Concurrency. Framework-ът е проектиран за композиция на асинхронни операции с гаранции за безопасност на нишките.

Основни точки

  • Combine — native реактивен framework на Apple с типове Publisher, Subscriber, Subject и типове грешки чрез Failure
  • Publisher излъчва събития във времето: Output стойности, може да завърши с успех или грешка Failure
  • Subscriber получава събития от Publisher и може да поиска определен брой елементи чрез Demand
  • Операторите map, flatMap, filter, combineLatest, zip, debounce трансформират потоци без състояния на състезание
  • SwiftUI използва Combine чрез @Published, ObservableObject и @StateObject за реактивно обновяване на интерфейса

Какво е Combine?

Combine — е декларативен реактивен програмен framework, вграден в Apple SDK. Той имплементира модела Reactive Streams: Publisher произвежда стойности, Subscriber ги консумира, а операторите трансформират потока между тях. Combine решава проблема с callback-ите и делегатите, предоставяйки единен модел за композиция за всякакви асинхронни събития — от мрежови отговори до промени в UI.

Преди появата на Combine, iOS разработчиците използваха библиотеки на трети страни, преди всичко RxSwift. Apple създаде Combine като native алтернатива с дълбока интеграция в екосистемата: framework-ът поддържа Objective-C чрез @objc мостове, работи с KVO (Key-Value Observing чрез NSObject.keyValuePublisher) и NotificationCenter, а също така е основа за SwiftUI. Всички UIKit компоненти, публикувани в SwiftUI, използват Combine под капака за обновяване на изгледите.

Combine е проектиран с оглед на Swift Concurrency: от iOS 15, Publisher може да бъде преобразуван в AsyncSequence чрез .values и използван в for-await-in цикли. Обратното преобразуване на async функции в Publisher се извършва чрез Future. Според Apple WWDC 2024, Combine остава препоръчителният framework за обработка на поточни данни в UIKit приложения, въпреки появата на async/await за единични асинхронни извиквания.

Основни концепции на Combine

Combine се основава на три протокола: Publisher (излъчва стойности от тип Output, може да завърши с грешка от тип Failure), Subscriber (получава стойности, управлява Demand — броя на заявените елементи), Subscription (представлява връзката Publisher-Subscriber с възможност за отмяна). Каналът за предаване на данни се инициализира при subscribe и завършва при cancel, completion или error. Demand — уникална концепция на Combine: Subscriber информира Publisher колко елемента е готов да обработи, имплементирайки backpressure на ниво протокол.

Publisher и Subscriber: архитектура на реактивния поток

Publisher — протокол с два асоциирани типа: Output (тип на излъчваните стойности) и Failure (тип грешка, имплементиращ Error). Ако потокът не може да завърши с грешка, Failure се посочва като Never — това гарантира на Subscriber, че onReceive ще бъде извикан само с Output. Вградените Publisher включват Just (една стойност), Sequence (масив), URLSession.DataTaskPublisher (мрежова заявка), NotificationCenter.Publisher и @Published property wrapper.

swift
import Combine

// Създаване на Publisher от последователност
let publisher = [1, 2, 3, 4, 5].publisher

// Създаване на Subscriber с обработка на стойности
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("Завършено")
    }
}

publisher.subscribe(PrintSubscriber())

Subscription и Demand

Subscription — е протокол, представляващ активната връзка между Publisher и Subscriber. Subscriber получава Subscription в метода receive(subscription:) и извиква request(_:) за указване на Demand: .unlimited (всички стойности), .max(N) (ограничен брой) или .none (пауза). Demand може да се променя динамично — Subscriber може да увеличи или намали броя на заявените елементи по време на получаване на данни. Това осигурява backpressure без буфериране от страна на Publisher.

Subject и CurrentValueSubject

Subject — е тип, който комбинира Publisher и Subscriber. Subject може да се използва като Publisher (може да се абонирате за него) и едновременно като Subscriber (могат да се изпращат стойности към него). Combine предоставя два вида Subject: PassthroughSubject (не съхранява състояние, предава само нови стойности) и CurrentValueSubject (съхранява текущата стойност и я предава на нови абонати). Subject е необходим за интегриране на императивен код в реактивните вериги на Combine.

swift
let subject = PassthroughSubject<String, Never>()

// Абонамент като Publisher
let cancellable = subject
    .map { $0.uppercased() }
    .sink { print($0) }

// Изпращане на стойности като Subscriber
subject.send("hello")  // Отпечатва „HELLO"
subject.send("свят")  // Отпечатва „WORLD"

CurrentValueSubject се различава от PassthroughSubject по наличието на начална стойност и свойство value: абонатът незабавно получава текущата стойност при абониране, а след това всички последващи актуализации. CurrentValueSubject.value е достъпен за четене и писане — промяната на value автоматично изпраща новата стойност на всички абонати. Това прави CurrentValueSubject идеален избор за представяне на състояние в MVVM архитектура: ViewModel публикува CurrentValueSubject, View се абонира с промени чрез sink.

И двата Subject позволяват прекратяване на потока чрез извикване на send(completion: .finished) или send(completion: .failure(error)). След прекратяване Subject спира да приема и предава събития. За дълготрайни потоци, които не трябва да приключват (напр. UI събития), се препоръчва използването на PassthroughSubject с Never Failure, за да се изключи случайно извикване на send(completion:).

Оператори за трансформация и комбиниране

Операторите Combine — са методи на Publisher, които връщат нов Publisher. Всеки оператор създава нов обект, който се абонира за Publisher-а нагоре по течението и излъчва трансформирани стойности надолу по течението. Тъй като Publisher е generic тип, операторите запазват строга типизация: map преобразува Output<A> в Output<B>, tryMap добавя възможност за грешка. Combine съдържа около 100 вградени оператора.

КатегорияОператорПредназначение
Трансформацияmap / tryMap / flatMapТрансформация на стойности или потоци
Филтриранеfilter / compactMap / removeDuplicatesСелекция или изчистване на стойности
КомбиниранеcombineLatest / zip / mergeКомбиниране на няколко Publisher
Управление на времетоdebounce / throttle / delayЗакъснение и разреждане на събития
Обработка на грешкиcatch / retry / replaceErrorВъзстановяване след Failure
Управление на Demandbuffer / collectГрупиране или буфериране

flatMap в Combine има важна разлика спрямо версията на RxSwift: приема closure, който връща Publisher със същия тип Failure и разгъва вложения Publisher в основния поток. flatMap с maxPublishers: .max(1) се държи като switchMap — отменя предишния вложен Publisher при пристигане на нова стойност. Това е критично за сценарии за търсене: при въвеждане на нов символ, предишната HTTP заявка се отменя автоматично.

swift
// Debounce търсене с отмяна на предишна заявка
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)

Оператори за комбиниране — combineLatest и zip — работят подобно на RxSwift: combineLatest излъчва кортеж от последните стойности на всички Publisher при промяна на който и да е от тях; zip комбинира стойностите по двойки по индекс. merge обединява Publisher от един тип в един поток, preserveOrder не е гарантирано. В Combine също има select — рядък оператор, който избира първия завършен Publisher от няколко, и share — multicast на потока към множество абонати без повторно изпълнение.

Schedulers и управление на нишките

Scheduler в Combine — е протокол, който определя контекста на изпълнение за операторите. За разлика от RxSwift с неговите 5+ вградени Scheduler, Combine използва съществуващите механизми на Apple: DispatchQueue, RunLoop и OperationQueue. Всеки от тези типове съответства на протокола Scheduler, което позволява директното им предаване на receive(on:) и subscribe(on:) без допълнителни адаптери.

receive(on:) превключва надолу по течението към указания Scheduler — еквивалент на observeOn в RxSwift. Всички оператори след receive(on:) се изпълняват на указания Scheduler. subscribe(on:) превключва нагоре по течението — влияе върху изпълнението на Publisher. Типичен модел: subscribe(on: DispatchQueue.global()) за фонова работа и receive(on: DispatchQueue.main) за обновяване на UI. В SwiftUI при използване на .onReceive не се изисква вградена връзка с основната нишка, но за sink се препоръчва изрично receive(on:).main.

swift
// Фоново изтегляне + UI на основната нишка
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 — алтернатива на DispatchQueue.main за UI операции. Разликата е, че RunLoop.main е обвързан с текущия цикъл на събития на приложението, докато DispatchQueue.main е обвързан с глобалната опашка на основната нишка. За UIKit се препоръчва DispatchQueue.main, за SwiftUI — RunLoop.main. ImmediateWhenScheduler изпълнява операциите синхронно на текущата нишка — използва се по подразбиране за тестове и прости Publisher.

Combine и SwiftUI: интеграция чрез ObservableObject

ObservableObject — SwiftUI протокол за обекти, които публикуват промени. Клас, имплементиращ ObservableObject, може да използва property wrapper @Published за свойства, чиито промени автоматично уведомяват SwiftUI за необходимост от прерисуване. Под капака, @Published създава Publisher, който уведомява Publisher objectWillChange при промяна на wrappedValue. SwiftUI се абонира за objectWillChange чрез @StateObject, @ObservedObject или @EnvironmentObject.

@Published и @StateObject

@Published — най-честият начин за интегриране на Combine в SwiftUI. Когато стойността на @Published свойство се промени, SwiftUI актуализира всички View, които използват този обект. @StateObject създава инстанция на ObservableObject и се абонира за нейните промени. View, създадена с @StateObject, автоматично се прерисува при промяна на @Published свойствата. Ако обектът трябва да се предава между множество View, се използва @ObservedObject или @EnvironmentObject.

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 — тип-изтривач за Cancellable, който съхранява токена за отмяна на абонамента. Set<AnyCancellable> управлява жизнения цикъл на абонаментите: при деинициализация на собственика, всички Cancellable се отменят автоматично. В SwiftUI проекти, Set<AnyCancellable> се декларира в класа ObservableObject, а абонаментите се добавят чрез .store(in: &cancellables). За UIKit се използват същите механизми със съхранение в UIViewController чрез &cancellables или ръчно извикване на cancel().

Combine vs RxSwift

Combine и RxSwift решават едни и същи задачи на реактивното програмиране, но имат фундаментални архитектурни различия. Combine — част от Apple SDK с обратна съвместимост до iOS 13, RxSwift — библиотека на трета страна с поддръжка на iOS 8+. Combine използва строга типизация на грешките чрез Failure generic, RxSwift — единния тип Error. Combine е интегриран със SwiftUI на ниво платформа, RxSwift изисква RxCocoa за UI разширения.

Изборът между Combine и RxSwift зависи от изискванията на проекта. Ако минималната версия на iOS >= 13 и проектът използва SwiftUI — Combine е естественият избор благодарение на вградената интеграция и липсата на допълнителни зависимости. Ако проектът поддържа iOS 11-12, съдържа съществуваща RxSwift кодова база или изисква специфични оператори, достъпни само в RxSwift (напр. Observable.from(path:)), — RxSwift остава оправдано решение.

ХарактеристикаCombineRxSwift
РазработчикApple (вграден в SDK)ReactiveX (общност)
iOS версияiOS 13+iOS 8+
Тип грешкаGeneric Failure (Never за UI)Error (произволен)
UI интеграция@Published + SwiftUIRxCocoa + UIKit
Оператори~100 вградени400+ оператора
Swift ConcurrencyЧрез .values (async sequence)Чрез мостова библиотека

Често задавани въпроси

Каква е разликата между PassthroughSubject и CurrentValueSubject?

PassthroughSubject не съхранява състояние — абонатът получава само събития, изпратени след абонирането. CurrentValueSubject съхранява текущата стойност и я предава на всеки нов абонат незабавно при абониране. CurrentValueSubject е подходящ за представяне на състояние (напр. isLoggedIn).

Как да отменя абонамент в Combine?

Абонаментът връща AnyCancellable, който се отменя при извикване на cancel() или при деинициализация. За групово управление използвайте Set<AnyCancellable> — всички абонаменти се отменят при изчистване на сета. Това е аналог на DisposeBag от RxSwift.

Трябва ли да уча Combine след async/await?

Да, Combine остава актуален за потокови данни: UI събития, debounce, combineLatest, WebSocket. async/await е удобен за единични заявки, Combine — за непрекъснати или множество потоци. И двата framework-а са взаимодопълващи се — Publisher може да бъде преобразуван в AsyncSequence.

Как да използвам Combine с UIKit?

UIKit няма вградени Publisher, но Apple предоставя разширения: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. За персонализирани UI събития се използват PassthroughSubject или @IBAction, увити в Publisher чрез Future или Subject.

Какво е backpressure в Combine?

Backpressure — механизъм за контрол на скоростта на потока: Subscriber информира Publisher чрез Demand колко елемента е готов да обработи. Ако Demand = .max(1), Publisher изчаква заявка преди да изпрати следващата стойност. Това предотвратява препълване на буфера при несъответствие на скоростите на производителя и потребителя.

Резюме

  • Combine — native реактивен framework на Apple за iOS 13+ с модел Publisher-Subscriber и строга типизация на грешки
  • Publisher и Subscriber формират комуникационен канал с управление на Demand за backpressure
  • Subject (Passthrough и CurrentValue) интегрират императивен код в реактивни вериги
  • Операторите map, flatMap, combineLatest, debounce, catch осигуряват декларативна обработка на потоци
  • Schedulers чрез DispatchQueue и RunLoop управляват нишките на изпълнение без блокиране на UI
  • SwiftUI използва Combine чрез @Published, ObservableObject и @StateObject за реактивни View
  • RxSwift остава алтернатива за проекти с iOS 8+, Combine — избор за нови проекти на iOS 13+

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също