Introduzione alla programmazione reattiva con Combina in iOS

Le applicazioni moderne iOS sono intrinsecamente asincroni. Dalle richieste di rete e dalle query di database agli eventi di interfaccia utente e ai dati dei sensori, gli sviluppatori devono gestire un flusso costante di informazioni che arrivano a tempi imprevedibili. Tradizionalmente, trattando questi eventi richiesti delegati, callback o centri di notifica, che spesso hanno portato a complesse modifiche di codice di errore.

Cos'è il Quadro Combinazione?

La combinazione è un framework nativo Swift che fornisce un API dichiarativo per il trattamento dei valori nel tempo. Fa parte dell’ecosistema di Apple e si integra perfettamente con SwiftUI, Foundation e UIKit. La combinazione è costruita su tre astrazioni principali: paccheri] emettere valori,

A differenza dei precedenti modelli come ] o KVO, Combine offre sicurezza di compilazione, inferenza di tipo e composizione potente. È simile nel concetto di estensioni reattive come RxSwift ma è di prima parte e profondamente integrato con le piattaforme di Apple.

Concetti core della combinazione

Prima di immergersi in codice, è fondamentale cogliere i quattro componenti primari che formano ogni pipeline Combine:

  • ] Editore[] – Un tipo che può emettere una sequenza di valori nel tempo. Dichiara che tipo di valori emette ([]) e che tipo di errori può fallire con ([[]]).
  • ]Subscriber[ – Un tipo che riceve valori da un editore. Definisce tre metodi: , (per gestire un nuovo valore), e (per gestire il successo o il fallimento).
  • Operatore[] – Un editore che trasforma i valori da un editore a monte. Operatori come [], , , e sono essi stessi editori che abbonano agli editori a monte, applicano una trasformazione e ridefiniscono il risultato.
  • Subscription[] – Il collegamento tra un editore e un abbonato. Un abbonamento controlla il flusso della domanda (backpressure). Gli abbonamenti possono essere annullati per interrompere la ricezione dei valori, che viene gestito automaticamente quando un viene negoziato o cancellato manualmente.

Inoltre, la combinazione fornisce Cancellable[] protocollo (e il tipo concreto [[]]) per gestire il ciclo di vita di un abbonamento.

Creazione di editori

Combina fornisce molti editori integrati che coprono le attività asincroni comuni. Puoi anche creare editori personalizzati implementando il protocollo ] o utilizzando inizializzatori di convenienza.

Timer Editor

Usa per emettere la data corrente a intervalli regolari. Per avviare immediatamente il timer, chiama :

let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

L'editore emette valori ogni secondo sul ciclo di esecuzione principale.

URLSession Data Task Editore

Combina si estende con un editore che esegue richieste di rete:

guard let url = URL(string: "https://api.example.com/data") else { return }
let publisher = URLSession.shared.dataTaskPublisher(for: url)

Questo editore emette una tuple al completamento, o un sul fallimento.

Notifica Center Editore

Converti qualsiasi notifica in un editore:

let notificationPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)

Utile per reagire agli eventi di sistema senza aggiungere manualmente gli osservatori.

Solo e futuro

emette un unico valore e poi completa immediatamente. E’ perfetto per testare o avvolgere una costante in un editore:

let justPublisher = Just("Hello, Combine!")

è un editore che produce un unico valore (o fallisce) asincronicamente, esegue la chiusura fornita esattamente una volta e memorizza il risultato per gli abbonati successivi:

let futurePublisher = Future<String, Error> { promise in
 // Perform async work, then call promise(.success(value)) or promise(.failure(error))
}

PassthroughSubject e Valuta correnteSubject

I soggetti agiscono sia come editore che come abbonato, permettendo di iniettare manualmente i valori in un flusso. trasmette i valori agli abbonati senza mantenere un valore corrente; mantiene l'ultimo valore e lo rigioca ai nuovi abbonati:

let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)

let currentValueSubject = CurrentValueSubject<String, Never>("initial")
currentValueSubject.send("updated")
print(currentValueSubject.value) // "updated"

Abbonamento a un editore

Per attivare un gasdotto, è necessario un abbonato. I più comunemente utilizzati sono e .

Il sottoscrittore del Seno

fornisce una gestione basata sulla chiusura per i valori ricevuti e gli eventi di completamento:

let subscription = timerPublisher.sink { time in
 print("Timer fired at \(time)")
}

È inoltre possibile gestire gli errori fornendo una chiusura :

publisher
 .sink(receiveCompletion: { completion in
 switch completion {
 case .finished:
 print("Stream finished")
 case .failure(let error):
 print("Error: \(error)")
 }
 }, receiveValue: { value in
 print("Received: \(value)")
 })

Il sottoscrittore dell'Assegna

lega l'output dell'editore a una proprietà di un oggetto, facoltativamente attraverso un percorso chiave. La proprietà deve essere contrassegnata con o essere conforme a KVO:

class ViewModel {
 @Published var text: String = ""
}

let viewModel = ViewModel()
let publisher = Just("Hello")
let cancellable = publisher.assign(to: \.text, on: viewModel)
print(viewModel.text) // "Hello"

Abbonamento personalizzato

Per i casi di uso avanzato, è possibile implementare il protocollo direttamente. Tuttavia, questo è raramente necessario; [ e coprire la maggior parte degli scenari.

Trasformazione dei dati con gli operatori

Gli operatori sono la spina dorsale della Combina, che consente di comporre condutture complesse da semplici blocchi di costruzione.

Mappa

Trasforma ogni valore emesso utilizzando una chiusura:

publisher
 .map { $0 * 2 }
 .sink { print($0) }

Filtro

Passa attraverso valori che soddisfano un predicato:

publisher
 .filter { $0.isMultiple(of: 2) }
 .sink { print("Even: \($0)") }

FlatMap

Apparato più editori interni in un unico flusso. Comunemente usato quando ogni valore innesca una nuova operazione asincrona:

let searchPublisher = searchTextPublisher
 .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
 .removeDuplicates()
 .flatMap { text in
 return api.searchPublisher(for: text)
 }

La mia ultima domanda

Combina gli ultimi valori da due o più editori in un tuple, emette un nuovo valore ogni volta che uno degli editori emette:

Publishers.CombineLatest(usernamePublisher, passwordPublisher)
 .map { username, password in
 return username.count > 0 && password.count > 0
 }
 .assign(to: \.isEnabled, on: loginButton)

Zip

Coppia valori da più editori in ordine, in attesa che tutti gli editori emettano il prossimo valore prima di emettere la coppia:

Publishers.Zip(emailsPublisher, namesPublisher)
 .sink { email, name in
 print("\(name): \(email)")
 }

Merge

Interleaves valori da più editori dello stesso tipo in un unico flusso:

Publishers.Merge(leftPublisher, rightPublisher)
 .sink { print($0) }

Collezioni

Buffer valori in array, emettendo una volta che il buffer riempie o il flusso completa:

publisher
 .collect(3) // emit arrays of 3 values
 .sink { print($0) }

Debunce e Throttle

attende una pausa specificata tra i valori prima di emettere l'ultimo. emette al massimo un valore per intervallo di tempo. Entrambi sono essenziali per la gestione di eventi ad alta frequenza come input di testo:

textField.textPublisher
 .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
 .sink { text in
 // Perform search after user stops typing
 }

Gestione degli errori in combinazione

Il tipo ] di un editore definisce gli errori che può emettere. Quando un editore non riesce, l'abbonamento viene terminato.

ProvareMap e provareFilter

Se la vostra chiusura di trasformazione può lanciare, utilizzare [] o . Il gasdotto non riesce se viene lanciato un errore:

publisher
 .tryMap { value in
 guard value > 0 else { throw InputError.invalid }
 return value * 2
 }

Prendi.

Sostituire un fallimento con un editore alternativo. Utile per fornire valori di ricaduta:

publisher
 .catch { error in
 return Just(0) // fallback value
 }

Retribuzioni

Risottoscrive automaticamente all'editore a monte un numero specificato di volte se non riesce:

publisher
 .retry(3)
 .sink(receiveCompletion: { ... }, receiveValue: { ... })

sostituireError e sostituireEmpty

sostituisce un guasto con un dato valore. fornisce un valore se l'editore completa senza emettere nulla.

Gestione della memoria e cancellazione

Ogni abbonamento consuma risorse e deve essere correttamente cancellato per evitare perdite di memoria. Combina fornisce , un riferimento cancellabile di tipo.

Conservare cancellabili in una collezione, in genere un , che viene trattato quando l'oggetto di proprietà (ad esempio, un controller di vista) è deinitializzato.

private var cancellables = Set<AnyCancellable>()

func setupBindings() {
 publisher
 .sink { [weak self] value in
 self?.updateUI(value)
 }
 .store(in: &cancellables)
}

Puoi anche chiamare manualmente su un cancellabile, che termina l'abbonamento e rilascia le risorse.

Esempio pratico completo: richiesta di rete con aggiornamenti dell'interfaccia utente

Costruiremo uno scenario reale: recuperare i dati degli utenti da un'API, decodificarlo e aggiornare una vista SwiftUI. Questo esempio dimostra la gestione degli errori, gli operatori e la gestione dei thread.

import SwiftUI
import Combine

struct User: Decodable, Identifiable {
 let id: Int
 let name: String
 let email: String
}

class UserViewModel: ObservableObject {
 @Published var users: [User] = []
 @Published var isLoading = false
 @Published var errorMessage: String? = nil

 private var cancellables = Set<AnyCancellable>()

 func fetchUsers() {
 isLoading = true
 errorMessage = nil

 guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
 errorMessage = "Invalid URL"
 isLoading = false
 return
 }

 URLSession.shared.dataTaskPublisher(for: url)
 .map { $0.data }
 .decode(type: [User].self, decoder: JSONDecoder())
 .receive(on: DispatchQueue.main)
 .sink(receiveCompletion: { [weak self] completion in
 self?.isLoading = false
 switch completion {
 case .finished:
 break
 case .failure(let error):
 self?.errorMessage = error.localizedDescription
 }
 }, receiveValue: { [weak self] users in
 self?.users = users
 })
 .store(in: &cancellables)
 }
}

Punti chiave:

  • estrae i dati grezzi dalla tuple di risposta.
  • utilizza l'operatore integrato di Combina per decodificare JSON. Se la decodifica non riesce, il gasdotto termina con un errore.
  • assicura che gli aggiornamenti dell'interfaccia utente avvengano sul filo principale.
  • L'iscrizione in lega il suo ciclo di vita al modello di vista.
  • L'errore viene visualizzato tramite proprietà, che può essere vincolato a un avviso in SwiftUI.

Combinazione e SwiftUI: un partenariato naturale

L’architettura reattiva di SwiftUI si basa fortemente su Combine. Proprietà annotate con crea editori che avvisano automaticamente SwiftUI quando cambiano. Gli operatori combinati consentono di trasformare i dati asincroni prima che raggiunga la vista. Ad esempio, è possibile debuncere l’ingresso di un campo di testo, quindi ottenere risultati di ricerca e aggiornare l’interfaccia utente usando Combine’s 74 o

Utilizzando Combine con SwiftUI elimina la necessità di schemi manuali KVO o delegare, con conseguente codice più prevedibile e testable.

Combinazione vs. altri framework reattivi

Prima di Combinare, gli sviluppatori iOS hanno spesso usato framework di terze parti come [RxSwift] o ReactiveCocoa].Combine offre diversi vantaggi: integrazione di prima parte, nessuna gestione della vigenza, interoperabilità senza soluzione di continuità con SwiftUI e Fondazione, e forte sicurezza del tipo.

Migliori Pratiche per l'utilizzo della combinazione

  • Le tubazioni sono semplici[[] – Le catene complesse possono essere difficili da debug.
  • Manage sottoscrizioni accuratamente[[] – Conservare sempre cancellabili ed evitare cicli di trattenere utilizzando in chiusure che catturano .
  • Utilizzare i programmatori appropriati[] – Usa per passare alla coda principale per gli aggiornamenti dell'interfaccia utente, e per eseguire lavori pesanti sulle code di sfondo.
  • Preferire [] per semplici attacchi di proprietà[[] – Riduce il codice della piastra caldaia.
  • Test your pipelines[[] – La provabilità della Combina è eccellente perché è possibile creare editori con o e verificare gli output utilizzando le aspettative.
  • Attenzione alla backpressure[[] – Il sistema a richiesta della Combina assicura agli abbonati il controllo del flusso. Gli operatori come [] o ] possono aiutare quando i dati arrivano più velocemente di quanto possa essere elaborato.

Conclusioni

Con la comprensione di editori, abbonati e operatori, è possibile costruire concisi, condotte reattive che sono più facili da ragionare e mantenere. Inizia con semplici esempi come rispondere a notifiche iOS o recuperare i dati di rete, quindi gradualmente esplorare gli operatori avanzati come , , e Swift è modelli di errore moderno di recupero.

For further reading, consult the official Apple Combine documentation, Hacking with Swift’s Combine introduction, or Ray Wenderlich’s Combine book for deeper dives.