Combine — βασικές έννοιες, Publisher και αντιδραστικός προγραμματισμός

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-05-03 Χρόνος ανάγνωσης: 11 λεπ

Combine — είναι ένα εγγενές πλαίσιο αντιδραστικού προγραμματισμού από την Apple, που παρουσιάστηκε στα iOS 13, macOS Catalina, tvOS 13 και watchOS 6. Παρέχει ένα δηλωτικό Swift API για την επεξεργασία ασύγχρονων γεγονότων μέσω του προτύπου Publisher και Subscriber, αντικαθιστώντας τους εκπροσώπους, τα closures και το 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 — είναι ένα δηλωτικό αντιδραστικό πλαίσιο προγραμματισμού ενσωματωμένο στο SDK της Apple. Υλοποιεί το πρότυπο Reactive Streams: ο Publisher παράγει τιμές, ο Subscriber τις καταναλώνει και οι τελεστές μετασχηματίζουν τη ροή μεταξύ τους. Το Combine λύνει το πρόβλημα των callbacks και των εκπροσώπων, παρέχοντας ένα ενοποιημένο μοντέλο σύνθεσης για οποιοδήποτε ασύγχρονο γεγονός — από αποκρίσεις δικτύου έως αλλαγές 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
Διαχείριση 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 (κοινότητα)
Έκδοση iOSiOS 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> — όλες οι εγγραφές ακυρώνονται κατά τον καθαρισμό του set. Αυτό είναι ανάλογο του 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 περιμένει αίτημα πριν στείλει την επόμενη τιμή. Αυτό αποτρέπει την υπερχείλιση buffer όταν οι ταχύτητες παραγωγού και καταναλωτή δεν ταιριάζουν.

Σύνοψη

  • Combine — εγγενές αντιδραστικό πλαίσιο της 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. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης