För att debounca en lokal Swift-beräkning avbryter du den föregående fördröjda uppgiften när indata ändras och beräknar sedan efter en kort paus. Den här macOS-handledningen bygger en syntetisk räknare för kvadratiska brickor som uppdateras först när den senast giltiga inmatningen har stabiliserats.
Definiera exakt vad kalkylatorn ska göra
Uppgiften för läsaren är enkel: lägg till en fördröjd förhandsvisning i ett nummerfält i en Mac-app. Föreställ dig en bild som består av kvadratiska lager: det första innehåller en bricka, det andra fyra och det tredje nio. När du anger ett antal lager beräknas det totala antalet brickor i lagren. Allt lagras i minnet och använder syntetiska tal.
Indatakravet för det här exemplet är:
- Acceptera ett heltal i bas tio från
0till och med1000. - Ta bort inledande och avslutande blanksteg före parsning.
- Visa en instruktion direkt vid tom inmatning eller ett fel vid ogiltig inmatning.
- Vid giltig inmatning ska det föregående svaret rensas och beräkningen vänta
300millisekunder. - Avbryt den väntande beräkningen när en ny redigering görs eller vyn försvinner.
Resultatet är summan av kvadraterna från 1 till och med det begärda antalet lager. Formeln är n * (n + 1) * (2 * n + 1) / 6. För 3 lager är den illustrativa beräkningen 1 + 4 + 9 = 14. För 0 är resultatet 0. Det här är matematiska exempel, inte uppmätta resultat från appen.
Intervallet på 300 millisekunder är ett exempel på ett interaktionsval. Det skapar en avslutande debounce: varje ny redigering startar om viloperioden. Kontinuerlig redigering kan skjuta upp beräkningen hur länge som helst. Välj det här beteendet när en förhandsvisning av stabiliserad inmatning är användbar; ett kommando som måste reagera på varje enskild redigering behöver andra krav.
Varför avbrytningen måste stoppa den fördröjda körvägen
En uppgiftshandtag låter appen avbryta tidigare schemalagt arbete. Swift-uppgifter kan börja köras så snart de skapas, och att släppa ett handtag stoppar inte uppgiften. Spara handtaget så att nästa redigering kan avbryta den. Apples dokumentation för Task beskriver båda beteendena.
Fördröjningen använder try await Task.sleep(for: .milliseconds(300)). Apple dokumenterar att detta pausar uppgiften utan att blockera dess underliggande tråd och kastar CancellationError om uppgiften avbryts innan fördröjningen är slut. Överlagringen som tar en varaktighet är tillgänglig i macOS 13 och senare. Dokumentation för Task.sleep
Avbrytning är fortfarande samarbetsbaserad: ett anrop till cancel() avbryter inte godtycklig beräkningskod med tvång. Den fördröjda körvägen måste returnera när sömnen kastar ett fel, och den bör kontrollera avbrytning igen före beräkningen. Dokumentation för Task.cancel, dokumentation för Task.checkCancellation
Ett vanligt misstag är att använda try? för sömnen och sedan fortsätta direkt till beräkningen. Då ignoreras det kastade avbrytningsfelet och efterföljande satser kan köras. Här avslutar felhanteringen uppgiften innan den kan producera ett inaktuellt svar.
Bygg det fullständiga macOS-exemplet
Skapa ett macOS-approjekt i Xcode med gränssnittet SwiftUI och språket Swift. Använd en Swift-verktygskedja av typen 6 och ange macOS 14 eller senare som distributionsmål. Det här exemplet använder den moderna överlagringen onChange(of:initial:_:) utan argument, som Apple anger är tillgänglig från macOS 14. Dokumentation för SwiftUI onChange
Ersätt den genererade appens startpunktsfil med hela programmet nedan. Ta bort den genererade filen ContentView.swift från målet eller ersätt dess innehåll så att det bara finns en deklaration av ContentView. Se till att det finns exakt en appdeklaration markerad med @main i målet.
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()
}
}
}
Vyn äger sin kalkylator med @StateObject. Apple dokumenterar att detta bevarar modellinstansen när containerns indata ändras, medan en ändring av vyidentiteten skapar en ny instans. Publicerade modellegenskaper styr den visade statusen och svaret. Dokumentation för StateObject
Modellen och uppgiftens avslutning är uttryckligen markerade med @MainActor. Apple definierar exekveraren för den här aktören som likvärdig med huvudanslutningens kö. I den här implementationen delar därför schemaläggning, avbrytning och tilldelning av resultat samma aktör. Sömnen pausar; det korta aritmetiska uttrycket körs på aktören efter väntan. Dokumentation för MainActor
Så förhindrar reglerna för indata och resultat inaktuella förhandsvisningar
Avbrytningen sker före valideringen. Om användaren ändrar ett giltigt tal till ett tomt fält eller ett ogiltigt värde avbryts fortfarande den äldre väntande beräkningen. En tidig retur från valideringen låter inte den äldre fördröjningen fortsätta mot ett synligt resultat.
Programmet rensar result vid varje redigering. Det är en avsiktlig visningsregel: svaret försvinner medan ett nytt värde väntar, så gränssnittet visar aldrig en gammal totalsumma som svar på den aktuella texten. En alternativ design skulle kunna behålla svaret med en tydlig etikett för föregående resultat, men då krävs andra visningsregler.
Foundation-metoden trimmingCharacters(in:) tar bort matchande tecken från båda ändarna av en sträng. Med .whitespacesAndNewlines blir inmatning som 3 till 3; tecken inuti strängen förblir oförändrade. Foundation-dokumentation för trimning
Swifts heltalsparser använder bas 10 som standard, accepterar ett valfritt inledande tecken och returnerar nil för ogiltiga eller ej representerbara värden. Därför godtas +3 och 003 här, medan 3.5, 1,000 och 3 0 avvisas. Ett parsad negativt värde avvisas av den separata intervallkontrollen. Dokumentation för Int.init
Den övre gränsen begränsar också aritmetiken. Vid 1000 lager är täljaren 2,003,001,000 och summan 333,833,500. Alla tillåtna inmatningar håller sig inom dessa icke-negativa mellanliggande gränser. Om du höjer gränsen måste du kontrollera mellanliggande multiplikationer såväl som slutresultatet; en division med sex i efterhand kan inte åtgärda en överrinnning som redan har inträffat.
Varje fördröjd uppgift fångar det validerade värdet layers för den redigeringen. Den läser inte om fältet efter sömnen. När uppgiften har klarat sin avbrytningskontroll finns ingen ytterligare await innan den tilldelar resultatet. Ett annat schemaläggningsanrop på huvudaktören kan inte komma emellan under den synkrona beräknings- och tilldelningssekvensen. Swifts språkguide förklarar aktörsisolering och uttryckligen markerade avbrottspunkter. Concurrency — The Swift Programming Language
Det här resonemanget förutsätter att beräkningen förblir synkron och mycket kort. Om du senare lägger till en asynkron operation ska du kontrollera avbrytning och om begäran fortfarande är aktuell när operationen har returnerat. En revisionsidentifierare kan skilja på efterföljande begäranden, även när användaren återgår till ett tidigare inmatningsvärde.
Kontrollera beteendet med syntetiska inmatningar
Bygg och kör appen och följ sedan den här godkännandesekvensen. Förväntade resultat följer av programmets krav; exakta uppvakningstider beror på schemaläggningen.
| Åtgärd | Förväntat beteende |
|---|---|
Öppna fönstret med den inledande inmatningen 3 |
Visa väntestatusen och sedan Total tiles: 14. |
Ersätt inmatningen med 4 |
Rensa föregående summa och visa sedan Total tiles: 30. |
Ange 1 och sedan 10 innan den första fördröjningen är slut |
Starta om fördröjningen; det stabiliserade värdet ger Total tiles: 385. |
Ange 4 och radera det sedan innan fördröjningen är slut |
Visa Enter a layer count. och ingen summa. |
Ange 3.5 eller 1001 |
Visa intervallinstruktionen direkt och ingen summa. |
Ange 0 |
Visa Total tiles: 0 efter fördröjningen. |
Ange 1000 |
Visa Total tiles: 333833500 efter fördröjningen. |
För att kontrollera avbrytning ska du göra nästa redigering med god marginal innan fördröjningen löper ut. Vid gränsen kan ett äldre resultat redan ha blivit färdigt innan nästa redigering behandlas. Nästa redigering rensar det ändå och startar den nya begäran. En fördröjning är ingen exakt tidpunkt för körning; Swifts språkguide beskriver sömn som en paus på minst det begärda tidsintervallet. Dokumentation om Swift-konkurrrens
Aritmetikexemplen ovan kontrollerades separat med heltalsberäkningar. En Swift-kompilator var inte tillgänglig i forskningsmiljön, så macOS-programmet granskades men kompilerades eller kördes inte där.
Knyt avbrytningen till vyns livscykel
När vyn försvinner anropas stop() uttryckligen. Apple dokumenterar att onDisappear körs efter att vyn har försvunnit, men den exakta tidpunkten beror på vytypen. Se det som vyns städhook snarare än en exakt tidsgräns för en fönsterhändelse. När vyn visas igen schemalägger utseendehanteraren den aktuella inmatningen på nytt. Dokumentation för SwiftUI onDisappear
Avbrytningshanteringen returnerar avsiktligt utan att ändra delad status eller rensa det lagrade handtaget. En äldre avbruten uppgift kan återupptas efter att ett nyare handtag har sparats; om den äldre uppgiften då utför ovillkorlig rensning kan den ta bort det nyare handtaget. I det här programmet rensar schemaläggnings- och stoppmetoderna avbrutna handtag, och endast en uppgift som slutförs utan avbrytning rensar sitt eget aktiva handtag.
Debouncing styr när arbetet börjar. Det gör inte en lång synkron beräkning responsiv, och avbrytning kan inte ångra ett resultat som redan har tilldelats. För den här begränsade formeln är det användbara mönstret kortfattat: avbryt det föregående handtaget, validera en fångad inmatning, pausa under viloperioden, avsluta vid avbrytning och spara den korta beräkningen på huvudaktören. Då får Mac-gränssnittet en förutsägbar förhandsvisning av stabiliserad inmatning.
Källor
- 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.
- Concurrency — The Swift Programming Language — Swift.org.
- onDisappear(perform:) — Apple.