Combine — је нативни фрејмворк реактивног програмирања компаније Apple, представљен у iOS 13, macOS Catalina, tvOS 13 и watchOS 6. Он пружа декларативни Swift API за обраду асинхроних догађаја кроз образац Publisher и Subscriber, замењујући делегате, затворења и NotificationCenter јединственим ланцем. Према Apple, 2025, Combine је темељ за SwiftUI и модерне iOS архитектуре, радећи у тесној вези са async/await и Structured Concurrency. Фрејмворк је дизајниран за композицију асинхроних операција са гаранцијама безбедности нити.
Главне тачке
Combine — је фрејмворк декларативног реактивног програмирања уграђен у Apple SDK. Он имплементира образац Reactive Streams: Publisher производи вредности, Subscriber их конзумира, а оператори трансформишу ток између њих. Combine решава проблем callback-ова и делегата, пружајући јединствени модел композиције за било које асинхроне догађаје — од мрежних одговора до промена UI.
Пре појаве Combine-а, iOS програмери су користили библиотеке трећих страна, пре свега RxSwift. Apple је створио Combine као нативну алтернативу са дубоком интеграцијом у екосистем: фрејмворк подржава 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 остаје препоручени фрејмворк за обраду токова података у UIKit апликацијама, упркос појави async/await за појединачне асинхроне позиве.
Combine се заснива на три протокола: Publisher (емитује вредности типа Output, може се завршити грешком типа Failure), Subscriber (прима вредности, управља Demand-ом — бројем захтеваних елемената), Subscription (представља везу Publisher-Subscriber са могућношћу отказивања). Канал преноса података се иницијализује при subscribe и завршава при cancel, completion или error. Demand — јединствени концепт Combine-а: Subscriber обавештава Publisher колико елемената је спреман да обради, имплементирајући backpressure на нивоу протокола.
Publisher — протокол са два повезана типа: Output (тип емитованих вредности) и Failure (тип грешке који имплементира Error). Ако ток не може да се заврши грешком, Failure се наводи као Never — ово гарантује Subscriber-у да ће onReceive бити позван само са Output. Уграђени Publisher-и укључују Just (једна вредност), Sequence (низ), URLSession.DataTaskPublisher (мрежни захтев), NotificationCenter.Publisher и @Published property wrapper.
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 — је протокол који представља активну везу између Publisher-а и Subscriber-а. Subscriber прима Subscription у методи receive(subscription:) и позива request(_:) за навођење Demand-а: .unlimited (све вредности), .max(N) (ограничен број) или .none (пауза). Demand се може динамички мењати — Subscriber може повећати или смањити број захтеваних елемената у процесу пријема података. Ово обезбеђује backpressure без баферовања на страни Publisher-а.
Subject — је тип који комбинује Publisher и Subscriber. Subject се може користити као Publisher (може се претплатити на њега) и истовремено као Subscriber (у њега се могу слати вредности). Combine пружа две врсте Subject-а: PassthroughSubject (не складишти стање, преноси само нове вредности) и CurrentValueSubject (складишти тренутну вредност и преноси је новим претплатницима). Subject је неопходан за интеграцију императивног кода у реактивне ланце Combine-а.
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 генерички тип, оператори задржавају строгу типизацију: 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-а |
| Управљање Demand-ом | buffer / collect | Груписање или баферовање |
flatMap у Combine-у има важну разлику у односу на RxSwift верзију: прихвата closure који враћа Publisher са истим Failure типом и развија угнежђени Publisher у главни ток. flatMap са maxPublishers: .max(1) понаша се као switchMap — отказује претходни угнежђени Publisher при доласку нове вредности. Ово је критично за сценарије претраге: при уносу новог знака, претходни HTTP захтев се аутоматски отказује.
// 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 — мултикаст тока на више претплатника без поновног извршења.
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.
// Позадинско учитавање + 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-е.
ObservableObject — SwiftUI протокол за објекте који објављују промене. Класа која имплементира ObservableObject може користити property wrapper @Published за својства чије промене аутоматски обавештавају SwiftUI о потреби за поновним цртањем. Испод хаубе, @Published ствара Publisher који обавештава Publisher objectWillChange при промени wrappedValue. SwiftUI се претплаћује на objectWillChange кроз @StateObject, @ObservedObject или @EnvironmentObject.
@Published — најчешћи начин интеграције Combine-а у SwiftUI. Када се вредност @Published својства промени, SwiftUI ажурира сва View-а која користе овај објекат. @StateObject креира инстанцу ObservableObject-а и претплаћује се на његове промене. View креиран са @StateObject се аутоматски поново црта при промени @Published својстава. Ако објекат треба да се преноси између више View-ова, користи се @ObservedObject или @EnvironmentObject.
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 и 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 остаје оправдано решење.
| Карактеристика | Combine | RxSwift |
|---|---|---|
| Програмер | Apple (уграђен у SDK) | ReactiveX (заједница) |
| iOS верзија | iOS 13+ | iOS 8+ |
| Тип грешке | Generic Failure (Never за UI) | Error (било која) |
| UI интеграција | @Published + SwiftUI | RxCocoa + UIKit |
| Оператори | ~100 уграђених | 400+ оператора |
| Swift Concurrency | Кроз .values (async sequence) | Кроз мост-библиотеку |
Често постављана питања
PassthroughSubject не складишти стање — претплатник добија само догађаје послате након претплате. CurrentValueSubject складишти тренутну вредност и преноси је сваком новом претплатнику одмах при претплати. CurrentValueSubject је погодан за представљање стања (нпр. isLoggedIn).
Претплата враћа AnyCancellable, који се отказује при позиву cancel() или при деиницијализацији. За групно управљање користите Set<AnyCancellable> — све претплате се отказују при чишћењу сета. Ово је аналог DisposeBag-у из RxSwift-а.
Да, Combine остаје релевантан за токове података: UI догађаји, debounce, combineLatest, WebSocket. async/await је згодан за појединачне захтеве, Combine — за непрекидне или вишеструке токове. Оба фрејмворка су комплементарна — Publisher се може трансформисати у AsyncSequence.
UIKit нема уграђене Publisher-е, али Apple пружа проширења: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. За прилагођене UI догађаје користе се PassthroughSubject или @IBAction умотани у Publisher кроз Future или Subject.
Backpressure — механизам контроле брзине тока: Subscriber обавештава Publisher кроз Demand колико елемената је спреман да обради. Ако је Demand = .max(1), Publisher чека захтев пре слања следеће вредности. Ово спречава препуњавање бафера при неподударању брзина произвођача и потрошача.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође