Combine è il framework nativo di programmazione reattiva di Apple, introdotto in iOS 13, macOS Catalina, tvOS 13 e watchOS 6. Fornisce un'API Swift dichiarativa per gestire eventi asincroni attraverso il pattern Publisher e Subscriber, sostituendo delegati, closure e NotificationCenter con una catena unificata. Secondo Apple, 2025, Combine è il fondamento per SwiftUI e le architetture iOS moderne, lavorando a stretto contatto con async/await e Structured Concurrency. Il framework è progettato per la composizione di operazioni asincrone con garanzie di thread-safety.
Punti Chiave
Combine è un framework di programmazione reattiva dichiarativa integrato nell'SDK Apple. Implementa il pattern Reactive Streams: Publisher produce valori, Subscriber li consuma e gli operatori trasformano il flusso tra di loro. Combine risolve il problema di callback e delegati fornendo un modello di composizione unificato per qualsiasi evento asincrono — dalle risposte di rete ai cambiamenti dell'interfaccia utente.
Prima di Combine, gli sviluppatori iOS utilizzavano librerie di terze parti, principalmente RxSwift. Apple ha creato Combine come alternativa nativa con profonda integrazione nell'ecosistema: il framework supporta Objective-C tramite bridge @objc, funziona con KVO (Key-Value Observing tramite NSObject.keyValuePublisher) e NotificationCenter, ed è anche la base per SwiftUI. Tutti i componenti UIKit pubblicati in SwiftUI utilizzano Combine internamente per gli aggiornamenti delle viste.
Combine è progettato tenendo conto di Swift Concurrency: a partire da iOS 15, un Publisher può essere convertito in AsyncSequence tramite .values e utilizzato in cicli for-await-in. La conversione inversa di funzioni async in Publisher viene eseguita tramite Future. Secondo Apple WWDC 2024, Combine rimane il framework consigliato per la gestione dei dati in streaming nelle applicazioni UIKit, nonostante l'introduzione di async/await per chiamate asincrone singole.
Combine si basa su tre protocolli: Publisher (emette valori di tipo Output, può fallire con un errore di tipo Failure), Subscriber (riceve valori, gestisce Demand — il numero di elementi richiesti), Subscription (rappresenta la connessione Publisher-Subscriber con capacità di cancellazione). Il canale di dati viene inizializzato all'iscrizione e termina con cancellazione, completamento o errore. Demand è un concetto unico di Combine: Subscriber comunica al Publisher quanti elementi è pronto a elaborare, implementando backpressure a livello di protocollo.
Publisher è un protocollo con due tipi associati: Output (il tipo di valori emessi) e Failure (il tipo di errore che implementa Error). Se il flusso non può fallire, Failure viene specificato come Never — questo garantisce al Subscriber che onReceive verrà chiamato solo con Output. I Publisher integrati includono Just (valore singolo), Sequence (array), URLSession.DataTaskPublisher (richiesta di rete), NotificationCenter.Publisher e il property wrapper @Published.
import Combine
// Creazione di Publisher da una sequenza
let publisher = [1, 2, 3, 4, 5].publisher
// Creazione di Subscriber con gestione dei valori
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("Completato")
}
}
publisher.subscribe(PrintSubscriber())
Subscription è un protocollo che rappresenta la connessione attiva tra Publisher e Subscriber. Il Subscriber riceve Subscription nel metodo receive(subscription:) e chiama request(_:) per specificare Demand: .unlimited (tutti i valori), .max(N) (quantità limitata) o .none (pausa). Demand può cambiare dinamicamente — il Subscriber può aumentare o diminuire il numero di elementi richiesti durante la ricezione dei dati. Questo fornisce backpressure senza buffering lato Publisher.
Subject è un tipo che combina Publisher e Subscriber. Un Subject può essere usato come Publisher (gli iscritti vi si sottoscrivono) e contemporaneamente come Subscriber (i valori vengono inviati ad esso). Combine fornisce due tipi di Subject: PassthroughSubject (non memorizza lo stato, passa solo nuovi valori) e CurrentValueSubject (memorizza il valore corrente e lo passa ai nuovi iscritti). Subject è necessario per integrare codice imperativo nelle catene reattive di Combine.
let subject = PassthroughSubject<String, Never>()
// Iscrizione come Publisher
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Invio di valori come Subscriber
subject.send("hello") // Stampa "HELLO"
subject.send("world") // Stampa "WORLD"
CurrentValueSubject differisce da PassthroughSubject per la presenza di un valore iniziale e una proprietà value: un iscritto riceve immediatamente il valore corrente all'iscrizione, e poi tutti gli aggiornamenti successivi. CurrentValueSubject.value è leggibile e scrivibile — modificare value invia automaticamente il nuovo valore a tutti gli iscritti. Questo rende CurrentValueSubject una scelta ideale per rappresentare lo stato nell'architettura MVVM: ViewModel pubblica CurrentValueSubject, View si iscrive con modifiche tramite sink.
Entrambi i Subject possono terminare il flusso chiamando send(completion: .finished) o send(completion: .failure(error)). Dopo la terminazione, il Subject smette di accettare ed emettere eventi. Per flussi di lunga durata che non dovrebbero terminare (ad esempio, eventi UI), si consiglia di utilizzare PassthroughSubject con Never Failure per evitare di chiamare accidentalmente send(completion:).
Operatori di Combine sono metodi di Publisher che restituiscono un nuovo Publisher. Ogni operatore crea un nuovo oggetto che si sottoscrive al Publisher upstream ed emette valori trasformati downstream. Poiché Publisher è un tipo generico, gli operatori mantengono una tipizzazione rigorosa: map trasforma Output<A> in Output<B>, tryMap aggiunge la possibilità di errore. Combine contiene circa 100 operatori integrati.
| Categoria | Operatore | Scopo |
|---|---|---|
| Trasformazione | map / tryMap / flatMap | Trasformare valori o flussi |
| Filtraggio | filter / compactMap / removeDuplicates | Selezionare o pulire valori |
| Combinazione | combineLatest / zip / merge | Combinare più Publisher |
| Controllo Temporale | debounce / throttle / delay | Ritardare e diradare eventi |
| Gestione Errori | catch / retry / replaceError | Recupero dopo Failure |
| Gestione Demand | buffer / collect | Raggruppamento o buffering |
flatMap in Combine ha una differenza importante rispetto alla versione RxSwift: accetta una closure che restituisce un Publisher con lo stesso tipo Failure e appiattisce il Publisher annidato nel flusso principale. flatMap con maxPublishers: .max(1) si comporta come switchMap — annulla il Publisher annidato precedente quando arriva un nuovo valore. Questo è critico per scenari di ricerca: quando viene inserito un nuovo carattere, la richiesta HTTP precedente viene automaticamente annullata.
// Ricerca con debounce e annullamento della richiesta precedente
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)
Operatori di combinazione — combineLatest e zip — funzionano in modo simile a RxSwift: combineLatest emette una tupla degli ultimi valori di tutti i Publisher quando uno qualsiasi cambia; zip abbina valori per indice. merge combina Publisher dello stesso tipo in un unico flusso, la preservazione dell'ordine non è garantita. Combine ha anche select — un operatore raro che sceglie il primo Publisher a completarsi tra diversi, e share — multicast di un flusso a più iscritti senza riesecuzione.
Scheduler in Combine è un protocollo che definisce il contesto di esecuzione per gli operatori. A differenza di RxSwift con i suoi 5+ Scheduler integrati, Combine utilizza meccanismi Apple esistenti: DispatchQueue, RunLoop e OperationQueue. Ciascuno di questi tipi è conforme al protocollo Scheduler, consentendo di passarli direttamente a receive(on:) e subscribe(on:) senza adattatori aggiuntivi.
receive(on:) commuta il downstream allo Scheduler specificato — equivalente di observeOn in RxSwift. Tutti gli operatori dopo receive(on:) vengono eseguiti sullo Scheduler specificato. subscribe(on:) commuta l'upstream — influisce sull'esecuzione del Publisher. Un pattern tipico: subscribe(on: DispatchQueue.global()) per lavoro in background e receive(on: DispatchQueue.main) per aggiornamenti UI. In SwiftUI, quando si usa .onReceive, il binding integrato al thread principale non è necessario, ma per sink si consiglia receive(on:).main esplicito.
// Caricamento in background + UI sul thread principale
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 è un'alternativa a DispatchQueue.main per operazioni UI. La differenza è che RunLoop.main è legato al ciclo di eventi corrente dell'applicazione, mentre DispatchQueue.main è legato alla coda globale del thread principale. Per UIKit si consiglia DispatchQueue.main, per SwiftUI — RunLoop.main. ImmediateWhenScheduler esegue operazioni in modo sincrono sul thread corrente — usato per impostazione predefinita per test e Publisher semplici.
ObservableObject è un protocollo SwiftUI per oggetti che pubblicano modifiche. Una classe che implementa ObservableObject può usare il property wrapper @Published per proprietà le cui modifiche notificano automaticamente SwiftUI della necessità di ridisegnare. Sotto il cofano, @Published crea un Publisher che notifica il Publisher objectWillChange quando wrappedValue cambia. SwiftUI si iscrive a objectWillChange tramite @StateObject, @ObservedObject o @EnvironmentObject.
@Published è il modo più comune per integrare Combine in SwiftUI. Quando il valore di una proprietà @Published cambia, SwiftUI aggiorna tutte le Views che utilizzano quell'oggetto. @StateObject crea un'istanza ObservableObject e si iscrive alle sue modifiche. Una View creata con @StateObject viene ridisegnata automaticamente quando le proprietà @Published cambiano. Se l'oggetto deve essere condiviso tra più Views, si utilizzano @ObservedObject o @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 è un wrapper di cancellazione del tipo per Cancellable che memorizza un token di cancellazione dell'iscrizione. Set<AnyCancellable> gestisce il ciclo di vita delle iscrizioni: quando il proprietario viene deinizializzato, tutti i Cancellable vengono automaticamente cancellati. Nei progetti SwiftUI, Set<AnyCancellable> viene dichiarato nella classe ObservableObject e le iscrizioni vengono aggiunte tramite .store(in: &cancellables). Per UIKit, vengono utilizzati gli stessi meccanismi con archiviazione in UIViewController tramite &cancellables o chiamate manuali a cancel().
Combine e RxSwift risolvono gli stessi compiti di programmazione reattiva ma hanno differenze architetturali fondamentali. Combine fa parte dell'SDK Apple con retrocompatibilità fino a iOS 13, RxSwift è una libreria di terze parti con supporto per iOS 8+. Combine utilizza una tipizzazione rigorosa degli errori tramite Failure generico, RxSwift utilizza un singolo tipo Error. Combine è integrato con SwiftUI a livello di piattaforma, RxSwift richiede RxCocoa per le estensioni UI.
La scelta tra Combine e RxSwift dipende dai requisiti del progetto. Se la versione minima di iOS è >= 13 e il progetto utilizza SwiftUI — Combine è la scelta naturale grazie all'integrazione incorporata e all'assenza di dipendenze aggiuntive. Se il progetto supporta iOS 11-12, contiene una base di codice RxSwift esistente o richiede operatori specifici disponibili solo in RxSwift (ad esempio, Observable.from(path:)), — RxSwift rimane una soluzione valida.
| Caratteristica | Combine | RxSwift |
|---|---|---|
| Sviluppatore | Apple (integrato nell'SDK) | ReactiveX (community) |
| Versione iOS | iOS 13+ | iOS 8+ |
| Tipo di Errore | Failure generico (Never per UI) | Error (qualsiasi) |
| Integrazione UI | @Published + SwiftUI | RxCocoa + UIKit |
| Operatori | ~100 integrati | 400+ operatori |
| Swift Concurrency | Tramite .values (sequenza async) | Tramite libreria ponte |
Domande Frequenti
PassthroughSubject non memorizza lo stato — un iscritto riceve solo gli eventi inviati dopo l'iscrizione. CurrentValueSubject memorizza il valore corrente e lo passa a ogni nuovo iscritto immediatamente all'iscrizione. CurrentValueSubject è adatto per rappresentare lo stato (ad esempio, isLoggedIn).
Un'iscrizione restituisce AnyCancellable, che viene annullato chiamando cancel() o alla deinizializzazione. Per la gestione di gruppo, utilizzare Set<AnyCancellable> — tutte le iscrizioni vengono annullate quando il set viene pulito. Questo è analogo a DisposeBag in RxSwift.
Sì, Combine rimane rilevante per i dati in streaming: eventi UI, debounce, combineLatest, WebSocket. async/await è comodo per richieste singole, Combine — per flussi continui o multipli. Entrambi i framework sono complementari — Publisher può essere convertito in AsyncSequence.
UIKit non ha Publisher integrati, ma Apple fornisce estensioni: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Per eventi UI personalizzati, vengono utilizzati PassthroughSubject o @IBAction avvolti in un Publisher tramite Future o Subject.
Backpressure è un meccanismo per controllare la velocità del flusso: Subscriber comunica al Publisher tramite Demand quanti elementi è pronto a elaborare. Se Demand = .max(1), Publisher attende una richiesta prima di inviare il valore successivo. Ciò previene l'overflow del buffer quando le velocità di produttore e consumatore non corrispondono.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche