Per applicare il debounce a un calcolo Swift locale, annulla l’attività ritardata precedente ogni volta che cambia l’input, quindi esegui il calcolo dopo una breve pausa. Questo tutorial per macOS crea un contatore sintetico di tessere quadrate che si aggiorna solo quando l’ultimo input valido si è stabilizzato.
Definisci esattamente cosa deve fare il calcolatore
Il compito per il lettore è semplice: aggiungere un’anteprima ritardata al campo numerico di un’app per Mac. Immagina una grafica composta da livelli quadrati: il primo contiene una tessera, il secondo quattro e il terzo nove. Inserendo il numero di livelli, si calcola il totale delle tessere presenti in tutti i livelli. Tutto rimane in memoria e usa numeri sintetici.
Per questo esempio, le regole dell’input sono:
- Accetta un intero in base 10 compreso tra
0e1000, estremi inclusi. - Rimuovi gli spazi iniziali e finali prima dell’analisi.
- Mostra subito un’istruzione se l’input è vuoto o un errore se non è valido.
- Per un input valido, cancella il risultato precedente e attendi
300millisecondi prima di eseguire il calcolo. - Annulla il calcolo in sospeso quando arriva un’altra modifica o la vista scompare.
Il risultato è la somma dei quadrati da 1 fino al numero di livelli richiesto. La formula è n * (n + 1) * (2 * n + 1) / 6. Per 3 livelli, il calcolo illustrativo è 1 + 4 + 9 = 14. Per 0, il risultato è 0. Si tratta di esempi matematici, non di risultati misurati dell’applicazione.
L’intervallo di 300 millisecondi è una scelta esemplificativa per l’interazione. Crea un debounce trailing: ogni nuova modifica riavvia il periodo di quiete. Una modifica continua può rimandare il calcolo all’infinito. Scegli questo comportamento quando è utile un’anteprima dopo che l’input si è stabilizzato; un comando che deve reagire a ogni singola modifica richiede regole diverse.
Perché l’annullamento deve interrompere il percorso ritardato
Un handle dell’attività consente all’app di annullare il lavoro programmato in precedenza. Le attività Swift possono iniziare a essere eseguite appena vengono create e scartare un handle non interrompe l’attività. Conserva l’handle in modo che la modifica successiva possa annullarla. La documentazione di Task di Apple descrive entrambi i comportamenti.
Il ritardo usa try await Task.sleep(for: .milliseconds(300)). Apple documenta che questa funzione sospende l’attività senza bloccare il thread sottostante e genera CancellationError se viene annullata prima che il ritardo termini. L’overload che accetta una durata è disponibile da macOS 13 in poi. Documentazione di Task.sleep
L’annullamento resta cooperativo: chiamare cancel() non interrompe forzatamente codice di calcolo arbitrario. Il percorso ritardato deve terminare quando sleep genera un errore e dovrebbe verificare di nuovo l’annullamento prima di calcolare. Documentazione di Task.cancel, documentazione di Task.checkCancellation
Un errore comune consiste nell’usare try? per sleep e poi proseguire direttamente con il calcolo. In questo modo si scarta l’errore di annullamento generato e si consente l’esecuzione delle istruzioni successive. Qui, intercettare l’errore fa uscire l’attività prima che possa produrre un risultato non aggiornato.
Crea l’esempio macOS completo
In Xcode, crea un progetto per un’app macOS usando l’interfaccia SwiftUI e il linguaggio Swift. Usa una toolchain Swift 6 e imposta come destinazione minima macOS 14 o successiva. Questo esempio usa il moderno overload onChange(of:initial:_:) senza argomenti, che Apple indica come disponibile da macOS 14. Documentazione di SwiftUI onChange
Sostituisci il file generato del punto di ingresso dell’app con il programma completo riportato sotto. Rimuovi dal target il file generato ContentView.swift oppure sostituiscine il contenuto, così che ci sia una sola dichiarazione ContentView. Nel target deve esserci una sola dichiarazione dell’app contrassegnata con @main.
import SwiftUI
import Combine
import Foundation
@MainActor
final class TileCalculator: ObservableObject {
@Published private(set) var result: Int?
@Published private(set) var status = "Enter a layer count."
private var pending: Task<Void, Never>?
func schedule(_ text: String) {
pending?.cancel()
pending = nil
result = nil
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else {
status = "Enter a layer count."
return
}
guard let layers = Int(cleaned, radix: 10),
(0...1000).contains(layers) else {
status = "Use a whole number from 0 to 1000."
return
}
status = "Waiting for typing to pause..."
pending = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
} catch {
return
}
guard let self else { return }
let total = layers * (layers + 1) * (2 * layers + 1) / 6
self.result = total
self.status = "Calculated for \(layers) layers."
self.pending = nil
}
}
func stop() {
pending?.cancel()
pending = nil
result = nil
status = "Calculation paused."
}
}
@MainActor
struct ContentView: View {
@State private var input = "3"
@StateObject private var calculator = TileCalculator()
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Square-layer tile counter")
.font(.headline)
TextField("Layers: 0 to 1000", text: $input)
.textFieldStyle(.roundedBorder)
Text(calculator.status)
if let total = calculator.result {
Text("Total tiles: \(total)")
.font(.title2)
}
}
.padding(24)
.frame(minWidth: 360, minHeight: 180)
.onAppear {
calculator.schedule(input)
}
.onChange(of: input) {
calculator.schedule(input)
}
.onDisappear {
calculator.stop()
}
}
}
@main
@MainActor
struct LocalTileCalculatorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
La vista gestisce il proprio calcolatore con @StateObject. Apple documenta che questo conserva l’istanza del modello anche quando cambiano gli input del contenitore, mentre un cambiamento dell’identità della vista crea una nuova istanza. Le proprietà pubblicate del modello determinano lo stato e il risultato visualizzati. Documentazione di StateObject
Il modello e la closure dell’attività sono contrassegnati esplicitamente con @MainActor. Apple definisce l’executor di questo attore come equivalente alla coda di dispatch principale. In questa implementazione, programmazione, annullamento e assegnazione del risultato condividono quindi lo stesso attore. sleep sospende l’esecuzione; dopo l’attesa, la breve espressione aritmetica viene eseguita su quell’attore. Documentazione di MainActor
Come le regole per input e risultati evitano anteprime non aggiornate
L’annullamento avviene prima della convalida. Se l’utente cambia un numero valido inserendo un campo vuoto o un valore non valido, il calcolo precedente ancora in sospeso viene comunque annullato. Uscire in anticipo dalla convalida non può lasciare attivo quel vecchio ritardo, che potrebbe produrre un risultato visibile.
Il programma cancella result a ogni modifica. È una scelta deliberata per la visualizzazione: il risultato scompare mentre il nuovo valore è in attesa, così l’interfaccia non presenta mai un totale precedente come risposta al testo corrente. Un design alternativo potrebbe mantenere il risultato con una chiara etichetta che lo identifichi come precedente, ma richiederebbe regole di visualizzazione diverse.
trimmingCharacters(in:) di Foundation rimuove da una stringa i caratteri corrispondenti alle estremità. Usando .whitespacesAndNewlines, un input come 3 diventa 3; i caratteri interni restano invariati. Documentazione di Foundation sul trimming
Il parser di interi di Swift usa per impostazione predefinita la base 10, accetta un segno iniziale facoltativo e restituisce nil per i valori non validi o non rappresentabili. Di conseguenza, qui vengono accettati +3 e 003, mentre 3.5, 1,000 e 3 0 vengono rifiutati. Un valore negativo analizzato viene rifiutato dal controllo separato dell’intervallo. Documentazione di Int.init
Il limite superiore vincola anche l’aritmetica. Con 1000 livelli, il numeratore è 2,003,001,000 e il totale è 333,833,500. Ogni input consentito resta entro questi limiti intermedi non negativi. Per aumentare il limite, occorre verificare anche le moltiplicazioni intermedie, oltre al risultato finale: dividere per sei in seguito non può correggere un overflow già avvenuto.
Ogni attività ritardata acquisisce il valore layers convalidato per quella modifica. Non rilegge il campo dopo la sospensione. Dopo aver superato il controllo di annullamento, non c’è un ulteriore await prima dell’assegnazione del risultato. Un’altra chiamata programmata sull’attore principale non può inserirsi tra il calcolo sincrono e l’assegnazione. La guida del linguaggio Swift spiega l’isolamento degli attori e i punti di sospensione indicati esplicitamente. Concorrenza — Il linguaggio di programmazione Swift
Questo ragionamento presuppone che il calcolo resti sincrono e molto breve. Se in seguito inserisci un’operazione asincrona, dopo il suo completamento verifica di nuovo l’annullamento e che la richiesta sia ancora quella corrente. Un identificatore di revisione può distinguere richieste successive, anche nei casi in cui l’utente torna a un valore di input precedente.
Verifica il comportamento con input sintetici
Compila ed esegui l’app, poi segui questa sequenza di verifica. I risultati attesi derivano dalle regole del programma; i tempi esatti di risveglio dipendono dalla pianificazione.
| Azione | Comportamento atteso |
|---|---|
Apri la finestra con l’input iniziale 3 |
Mostra lo stato di attesa, quindi Total tiles: 14. |
Sostituisci l’input con 4 |
Cancella il totale precedente, quindi mostra Total tiles: 30. |
Inserisci 1, quindi 10 prima che termini il primo ritardo |
Riavvia il ritardo; il valore stabilizzato produce Total tiles: 385. |
Inserisci 4, quindi cancellalo prima che termini il ritardo |
Mostra Enter a layer count. e nessun totale. |
Inserisci 3.5 o 1001 |
Mostra subito l’istruzione sull’intervallo e nessun totale. |
Inserisci 0 |
Dopo il ritardo, mostra Total tiles: 0. |
Inserisci 1000 |
Dopo il ritardo, mostra Total tiles: 333833500. |
Per verificare l’annullamento, effettua la modifica successiva con un buon margine prima che scada il ritardo. In prossimità del limite, un risultato precedente potrebbe essere già stato completato prima che venga elaborata la modifica successiva. La modifica successiva lo cancellerà comunque e avvierà la nuova richiesta. Un ritardo non è un appuntamento esatto per l’esecuzione; la guida del linguaggio Swift descrive sleep come una sospensione di almeno l’intervallo richiesto. Documentazione sulla concorrenza in Swift
Gli esempi aritmetici sopra sono stati verificati in modo indipendente con calcoli interi. Nell’ambiente di ricerca non era disponibile un compilatore Swift, quindi il programma macOS è stato esaminato ma non compilato né eseguito.
Mantieni l’annullamento legato al ciclo di vita della vista
Il gestore di scomparsa chiama esplicitamente stop(). Apple documenta che onDisappear viene eseguito dopo che la vista è scomparsa, con tempistiche precise che dipendono dal tipo di vista. Consideralo un hook di pulizia della vista, non una scadenza precisa legata agli eventi della finestra. Il gestore di apparizione riprogramma l’input corrente ogni volta che la vista appare. Documentazione di SwiftUI onDisappear
La gestione dell’errore di annullamento termina deliberatamente senza modificare lo stato condiviso né cancellare l’handle memorizzato. Un’attività annullata in precedenza potrebbe riprendere dopo che è stata memorizzata una nuova attività; se quella vecchia eseguisse una pulizia incondizionata, potrebbe cancellare il nuovo handle. In questo programma, i metodi di pianificazione e arresto cancellano gli handle annullati e solo un’attività che termina correttamente cancella il proprio handle attivo.
Il debounce controlla quando inizia il lavoro. Non rende reattivo un calcolo sincrono lungo, né può annullare un risultato già assegnato. Per questa formula con limiti definiti, il modello utile è compatto: annulla l’handle precedente, convalida un input acquisito, sospendi per il periodo di quiete, termina se viene annullato e assegna il breve risultato del calcolo sull’attore principale. Così l’interfaccia del Mac offre un’anteprima prevedibile dopo che l’input si è stabilizzato.
Fonti
- Task — Apple.
- sleep(for:tolerance:clock:) — Apple.
- cancel() — Apple.
- checkCancellation() — Apple.
- onChange(of:initial:_:) — Apple.
- StateObject — Apple.
- MainActor — Apple.
- trimmingCharacters(in:) — Apple.
- init(_:radix:) — Apple.
- Concorrenza — Il linguaggio di programmazione Swift — Swift.org.
- onDisappear(perform:) — Apple.