Table of Contents
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.