Combine — concetti chiave, Publisher e programmazione reattiva

Autore: IT Sectr Pubblicato: 2026-05-03 Tempo di lettura: 11 min

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 è il framework reattivo nativo di Apple con tipi Publisher, Subscriber, Subject e tipi di errore tramite Failure
  • Publisher emette eventi nel tempo: valori Output, può terminare con successo o errore Failure
  • Subscriber riceve eventi dal Publisher e può richiedere un numero specifico di elementi tramite Demand
  • Operatori map, flatMap, filter, combineLatest, zip, debounce trasformano flussi senza condizioni di gara
  • SwiftUI usa Combine tramite @Published, ObservableObject e @StateObject per aggiornamenti reattivi dell'interfaccia

Cos'è Combine?

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.

Concetti Fondamentali di Combine

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 e Subscriber: Architettura del Flusso Reattivo

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.

swift
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 e Demand

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 e CurrentValueSubject

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.

swift
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 Trasformazione e Combinazione

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.

CategoriaOperatoreScopo
Trasformazionemap / tryMap / flatMapTrasformare valori o flussi
Filtraggiofilter / compactMap / removeDuplicatesSelezionare o pulire valori
CombinazionecombineLatest / zip / mergeCombinare più Publisher
Controllo Temporaledebounce / throttle / delayRitardare e diradare eventi
Gestione Erroricatch / retry / replaceErrorRecupero dopo Failure
Gestione Demandbuffer / collectRaggruppamento 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.

swift
// 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 e Gestione dei Thread

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.

swift
// 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.

Combine e SwiftUI: Integrazione tramite ObservableObject

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 e @StateObject

@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.

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 è 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 vs RxSwift

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.

CaratteristicaCombineRxSwift
SviluppatoreApple (integrato nell'SDK)ReactiveX (community)
Versione iOSiOS 13+iOS 8+
Tipo di ErroreFailure generico (Never per UI)Error (qualsiasi)
Integrazione UI@Published + SwiftUIRxCocoa + UIKit
Operatori~100 integrati400+ operatori
Swift ConcurrencyTramite .values (sequenza async)Tramite libreria ponte

Domande Frequenti

Qual è la differenza tra PassthroughSubject e CurrentValueSubject?

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).

Come annullare un'iscrizione in Combine?

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.

Devo imparare Combine dopo async/await?

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.

Come usare Combine con UIKit?

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.

Cos'è il backpressure in Combine?

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

  • Combine è il framework reattivo nativo di Apple per iOS 13+ con pattern Publisher-Subscriber e tipizzazione rigorosa degli errori
  • Publisher e Subscriber formano un canale di comunicazione con gestione Demand per il backpressure
  • Subject (Passthrough e CurrentValue) integrano codice imperativo in catene reattive
  • Operatori map, flatMap, combineLatest, debounce, catch forniscono elaborazione dichiarativa dei flussi
  • Scheduler tramite DispatchQueue e RunLoop gestiscono i thread di esecuzione senza bloccare l'UI
  • SwiftUI usa Combine tramite @Published, ObservableObject e @StateObject per Views reattive
  • RxSwift rimane un'alternativa per progetti con iOS 8+, Combine è la scelta per nuovi progetti su iOS 13+

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.

Discuti il progetto

Leggi anche