Combine е рамка за реактивно програмиране от Apple, представена в iOS 13. Според Apple Documentation, 2025, Combine предоставя единен декларативен API за обработка на асинхронни събития. Publisher определя източника на данни, а Subscriber се абонира за получаването им.
Основни
Combine е декларативна Swift рамка за обработка на асинхронни събития във времето. За разлика от подходите, базирани на callback, Combine позволява описване на тръбопроводи за обработка на данни чрез верига от оператори. Рамката е интегрирана с Foundation (URLSession, Timer, NotificationCenter) и SwiftUI (ObservableObject, @Published).
Всеки тип, имплементиращ протокола Publisher, трябва да дефинира Output (тип стойности) и Failure (тип грешка). Publisher не е активен до абониране — започва да излъчва събития едва след извикване на subscribe. Това е студена семантика, характерна за реактивни потоци, повишаваща ефективността.
Subscriber получава събития чрез три метода: receive(subscription:) — потвърждение на абонамент с управление на demand; receive(_:) — получаване на нова стойност; receive(completion:) — завършване на потока с успех или грешка. Demand определя броя стойности, които Subscriber е готов да приеме — това е механизмът backpressure.
Apple предоставя множество вградени Publisher: Just излъчва една стойност и завършва, Future — асинхронен резултат със затваряне, Deferred — отложено създаване на Publisher до момента на абониране. URLSession.dataTaskPublisher преобразува мрежова заявка в реактивен поток. Timer.publish създава периодичен таймер. NotificationCenter.default.publisher превръща известията в Publisher. Тези интеграции позволяват абониране за събития без ръчен мост.
В Combine Publisher и Subscriber са свързани чрез протокола Subscription. Subscription е свързващ обект, който контролира потока от данни. Разработчикът не трябва да създава свои типове Publisher — Apple предоставя вградени: Just, Future, Deferred, Fail, Empty, както и Publisher за URLSession и Timer.
Subject е Publisher, в който могат ръчно да се изпращат стойности. PassthroughSubject не съхранява състояние: абонатът получава само събития, настъпили след абонирането. CurrentValueSubject, аналогичен на BehaviorRelay в RxSwift, съхранява текущата стойност и незабавно я предава на новия абонат.
Механизмът backpressure в Combine регулира скоростта на предаване на данни. Subscriber уведомява Publisher чрез subscription.request(.unlimited) или subscription.request(.max(N)) колко стойности е готов да обработи. Това предотвратява препълване на буфера при неравномерна скорост на производител и потребител.
Combine включва над 100 оператора, разделени в категории: трансформация (map, tryMap, flatMap), филтриране (filter, compactMap, removeDuplicates), комбиниране (combineLatest, merge, zip), управление на времето (debounce, throttle, delay) и обработка на грешки (catch, replaceError, retry).
Операторът combineLatest комбинира последните стойности на два Publisher — всеки път, когато някой от тях излъчи нова стойност, затварянето се извиква с двете актуализирани стойности. Този оператор е незаменим при валидиране на формуляри, където трябва да се проследява състоянието на няколко полета за въвеждане едновременно.
Операторът debounce забавя публикуването на стойности, докато не изтече определен интервал без нови събития. Това е критично за полета за търсене: заявката към сървъра се изпраща едва след като потребителят спре да пише за 300-500 ms, което намалява натоварването 5-10 пъти.
Combine предлага няколко стратегии за обработка на грешки на ниво оператори. catch прихваща Failure и замества Publisher с резервен — потокът продължава без срив. replaceError(with:) поставя стойност по подразбиране вместо грешката. retry(_:) повтаря абонамента определен брой пъти при неуспех. Важно: типът грешка Never в подписа на Publisher гарантира, че потокът никога няма да завърши с грешка — това позволява използването на Publisher в SwiftUI без обработка на Failure.
Първият пример показва основна работа с Just — Publisher, който излъчва една стойност и завършва:
let publisher = Just("Hello Combine")
publisher
.sink(receiveCompletion: { completion in
print("Завършено: \(completion)")
}, receiveValue: { value in
print("Получено: \(value)")
})
Вторият пример демонстрира интеграцията на Combine с URLSession за мрежова заявка с обработка на резултата в основната нишка:
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("Грешка: \(error)")
}
}, receiveValue: { data in
print("Данни получени: \(data.count)")
})
Третият пример — използване на CurrentValueSubject за управление на състоянието:
let counter = CurrentValueSubject<Int, Never>(0)
counter
.sink { value in
print("Брояч: \(value)")
}
counter.send(1) // Ще изведе: Брояч: 1
counter.send(2) // Ще изведе: Брояч: 2
print(counter.value) // Ще изведе: 2
Combine и RxSwift решават една и съща задача — реактивно програмиране, но с различни подходи. Combine е родна рамка на Apple, вградена в системата: не изисква инсталиране на зависимости, интегрирана е с SwiftUI и Foundation. RxSwift е библиотека на трета страна с поддръжка на iOS 9+ и по-широк набор от оператори.
Производителността на Combine е по-висока в типични сценарии благодарение на родната имплементация на C. RxSwift предоставя повече инструменти за отстраняване на грешки (RxSwift.Resources.total, Debug), но неговата обектно-ориентирана архитектура може да бъде по-малко ефективна при големи потоци от данни. Изборът между тях е компромис между възможности и интеграция.
Важна разлика — моделът backpressure. В Combine Subscriber управлява demand чрез Subscription, което дава изричен контрол върху скоростта на потока. В RxSwift backpressure не се контролира по подразбиране — Observable излъчва всички стойности, а Subscriber ги обработва при постъпване. За големи потоци от данни Combine се оказва по-предвидим и по-безопасен по отношение на паметта.
Combine е дълбоко интегриран с SwiftUI на езиково ниво. @Published генерира Publisher, ObservableObject предоставя objectWillChange Publisher. SwiftUI View автоматично се абонира за тези Publisher и се прерисува при промени. За UIKit съществуват външни обвивки като CombineCocoa, но Combine първоначално е проектиран за SwiftUI, така че интеграцията му в UIKit изисква допълнителен код. RxSwift, обратно, има богата екосистема за UIKit: RxCocoa предоставя реактивни обвивки за всички UI елементи.
Combine поддържа интеграция с Core Data чрез NSFetchedResultsController Publisher. Publisher излъчва нови данни при всяка промяна на стека Core Data: добавяне, изтриване, актуализиране на записи. Това позволява изграждане на реактивни списъци без ръчно извикване на reloadData. Комбинирайки Combine с NotificationCenter Publisher за UIApplication.willResignActiveNotification, разработчикът създава реактивен слой за целия жизнен цикъл на приложението без делегати и callback-и. За класове ObservableObject SwiftUI автоматично актуализира View при промяна на @Published свойства — Combine осигурява цялата връзка без нито един ред ръчен код. Модулното тестване на Combine код се извършва чрез XCTestExpectation и Publisher.sink в тестови случаи с изолация на Scheduler чрез ImmediateScheduler.
При работа с Combine в UIKit проекти, разработчиците често използват абонамент чрез sink и съхраняват AnyCancellable в Set. Типичен модел: ViewController създава изглед, съхранява cancellables, абонира се за Publisher от ViewModel. При deinit всички абонаменти автоматично се отменят чрез колекцията Cancellables. За правилна работа с UIControl в Combine може да се използва UIControl.Event Publisher чрез разширението publisher(for:).
Отстраняването на грешки в Combine се осъществява чрез оператора print() или handleEvents(). print(String) регистрира всички събития: receive subscription, request demand, receive value, receive completion. handleEvents дава по-фин контрол: могат да се зададат затваряния за всеки етап от жизнения цикъл на Publisher. За тестване на Combine съществува CombineExpectations — библиотека, която позволява проверка дали Publisher излъчва очакваните стойности в тестове с виртуално време и контрол на demand.
Combine се използва активно в SwiftUI за управление на формуляри: всяко @Published свойство генерира Publisher, SwiftUI се абонира за него чрез View.body. Комбинирайки множество Publisher чрез combineLatest, валидирането на формуляра става декларативно: всеки Publisher проследява едно поле, combineLatest събира всички стойности, map ги проверява и връща състоянието на бутона Submit. Publishers.Merge обединява няколко Publisher от един тип в един — полезно за събиране на събития от различни контроли в един поток.
За интеграция на Combine с UIKit използвайте UIControl Publisher чрез разширение: button.publisher(for: .touchUpInside) връща Publisher, който излъчва събитие при натискане. CombineCocoa — библиотека от общността, предоставяща Publisher за всички UIControl събития, UITextView.text, UIScrollView.contentOffset. При отписване от абонамент, cancellables Store се изчиства в deinit, гарантирайки, че нито един абонамент няма да надживее своя собственик. За UIControl с множество състояния използвайте Publishers.MergeMany за обединяване на Publisher в масив.
Често задавани въпроси
Combine е достъпен от iOS 13, macOS 10.15, tvOS 13 и watchOS 6. Това ограничава използването му в проекти, изискващи поддръжка на по-стари версии. За iOS 12 и по-долу се прилага RxSwift или библиотеки на трети страни.
PassthroughSubject не съхранява състояние и не повтаря последната стойност на нови абонати — те получават само бъдещи събития. CurrentValueSubject съхранява текущата стойност и незабавно я предава на новия абонат. CurrentValueSubject.value позволява четене и запис на текущата стойност синхронно.
Combine стои в основата на SwiftUI: @Published генерира Publisher, ObservableObject използва objectWillChange Publisher. SwiftUI View автоматично се абонира за @ObservedObject и @StateObject чрез Combine тръбопровода, прерисувайки View при промяна на свойствата.
Backpressure — механизъм за управление на скоростта на предаване на данни. Subscriber уведомява Subscription колко стойности е готов да обработи (demand). Publisher не може да излъчи повече от поисканото. Това е критично при работа с големи потоци от данни от мрежови сокети.
Операторът catch прихваща грешката и замества Publisher с fallback. replaceError замества грешката със стойност по подразбиране. retry повтаря абонамента определен брой пъти при грешка. Всички оператори трябва да се поставят преди subscribe, за да се изключи срив при Failure.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също