Combine — кључни појмови, Publisher и реактивно програмирање

Аутор: IT Sectr Објављено: 2026-05-03 Време читања: 11 мин

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-а са типовима Publisher, Subscriber, Subject и типовима грешака кроз Failure
  • Publisher емитује догађаје током времена: Output вредности, може се завршити успехом или грешком Failure
  • Subscriber прима догађаје од Publisher-а и може захтевати одређени број елемената кроз Demand
  • Оператори map, flatMap, filter, combineLatest, zip, debounce трансформишу токове без стања трке
  • SwiftUI користи Combine кроз @Published, ObservableObject и @StateObject за реактивно ажурирање интерфејса

Шта је Combine?

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-а

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 генерички тип, оператори задржавају строгу типизацију: 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 захтев се аутоматски отказује.

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 — мултикаст тока на више претплатника без поновног извршења.

Scheduler-и и управљање нитима

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 — за непрекидне или вишеструке токове. Оба фрејмворка су комплементарна — 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 — нативни реактивни фрејмворк Apple-а за iOS 13+ са обрасцем Publisher-Subscriber и строгом типизацијом грешака
  • Publisher и Subscriber формирају канал комуникације са управљањем Demand-ом за backpressure
  • Subject (Passthrough и CurrentValue) интегришу императивни код у реактивне ланце
  • Оператори map, flatMap, combineLatest, debounce, catch обезбеђују декларативну обраду токова
  • Scheduler-и кроз DispatchQueue и RunLoop управљају нитима извршења без блокирања UI
  • SwiftUI користи Combine кроз @Published, ObservableObject и @StateObject за реактивна View-а
  • RxSwift остаје алтернатива за пројекте са iOS 8+, Combine — избор за нове пројекте на iOS 13+

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође