Um eine lokale Swift-Berechnung zu entprellen, brich die vorherige verzögerte Aufgabe bei jeder Eingabeänderung ab und berechne das Ergebnis nach einer kurzen Pause. In diesem macOS-Tutorial wird ein synthetischer Zähler für quadratische Kacheln erstellt, der erst aktualisiert wird, wenn die letzte gültige Eingabe stabil bleibt.
Genau festlegen, was der Rechner tun soll
Die Aufgabe für den Leser ist einfach: einer Zahlenfelder-App auf dem Mac eine verzögerte Vorschau hinzufügen. Stell dir eine Grafik aus quadratischen Ebenen vor: Die erste enthält eine Kachel, die zweite vier und die dritte neun. Durch Eingabe einer Ebenenzahl wird die Gesamtzahl der Kacheln über alle Ebenen hinweg berechnet. Alles bleibt im Arbeitsspeicher und verwendet synthetische Zahlen.
Für dieses Beispiel gelten folgende Eingaberegeln:
- Eine Ganzzahl zur Basis 10 von
0bis einschließlich1000akzeptieren. - Vor dem Parsen führende und nachgestellte Leerzeichen entfernen.
- Bei leerer Eingabe sofort eine Anweisung und bei ungültiger Eingabe eine Fehlermeldung anzeigen.
- Bei gültiger Eingabe das vorherige Ergebnis löschen und vor der Berechnung
300Millisekunden warten. - Die ausstehende Berechnung abbrechen, sobald eine weitere Änderung eintrifft oder die Ansicht verschwindet.
Das Ergebnis ist die Summe der Quadrate von 1 bis zur angeforderten Ebenenzahl. Die Formel lautet n * (n + 1) * (2 * n + 1) / 6. Für 3 Ebenen lautet die beispielhafte Berechnung 1 + 4 + 9 = 14. Für 0 ist das Ergebnis 0. Dies sind mathematische Beispiele, keine gemessenen Anwendungsergebnisse.
Das Intervall von 300 Millisekunden ist eine beispielhafte Wahl für die Interaktion. Es erzeugt eine nachlaufende Entprellung: Jede neue Änderung startet die Ruhezeit erneut. Bei kontinuierlicher Bearbeitung kann sich die Berechnung unbegrenzt verzögern. Wähle dieses Verhalten, wenn eine Vorschau nach Abschluss der Eingabe nützlich ist; ein Befehl, der auf jede einzelne Änderung reagieren muss, benötigt andere Regeln.
Warum der Abbruch den verzögerten Ablauf beenden muss
Mit einem Task-Handle kann die App zuvor geplante Arbeit abbrechen. Swift-Tasks können sofort nach ihrer Erstellung ausgeführt werden, und das Verwerfen eines Handles stoppt den Task nicht. Speichere das Handle, damit der nächste Bearbeitungsschritt den Task abbrechen kann. Apples Dokumentation zu Task beschreibt beide Verhaltensweisen.
Für die Verzögerung wird try await Task.sleep(for: .milliseconds(300)) verwendet. Apple dokumentiert, dass dadurch der Task angehalten wird, ohne seinen zugrunde liegenden Thread zu blockieren, und dass bei einem Abbruch vor Ende der Verzögerung CancellationError ausgelöst wird. Die Duration-Überladung ist ab macOS 13 verfügbar. Dokumentation zu Task.sleep
Der Abbruch erfolgt weiterhin kooperativ: Ein Aufruf von cancel() unterbricht beliebigen Berechnungscode nicht gewaltsam. Der verzögerte Ablauf muss zurückkehren, wenn das Schlafen einen Fehler auslöst, und sollte vor der Berechnung erneut auf einen Abbruch prüfen. Dokumentation zu Task.cancel, Dokumentation zu Task.checkCancellation
Ein häufiger Fehler ist, für das Schlafen try? zu verwenden und anschließend direkt mit der Berechnung fortzufahren. Dadurch wird der ausgelöste Abbruchfehler verworfen, und die folgenden Anweisungen werden trotzdem ausgeführt. Hier beendet das Abfangen des Fehlers den Task, bevor er ein veraltetes Ergebnis ausgeben kann.
Das vollständige macOS-Beispiel erstellen
Erstelle in Xcode ein macOS-App-Projekt mit der Oberfläche SwiftUI und der Sprache Swift. Verwende eine Swift-Toolchain 6 und setze das Deployment-Ziel auf macOS 14 oder neuer. Dieses Beispiel verwendet die moderne Überladung von onChange(of:initial:_:) ohne Argumente, die laut Apple ab macOS 14 verfügbar ist. Dokumentation zu SwiftUI onChange
Ersetze die generierte App-Einstiegsdatei durch das vollständige Programm unten. Entferne die generierte Datei ContentView.swift aus dem Target oder ersetze ihren Inhalt, sodass nur eine Deklaration von ContentView vorhanden ist. Behalte genau eine App-Deklaration mit dem Attribut @main im Target.
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()
}
}
}
Die Ansicht besitzt ihren Rechner über @StateObject. Apple dokumentiert, dass dadurch die Modellinstanz bei Änderungen der Eingaben des Containers erhalten bleibt, während eine Änderung der Ansichtsidentität eine neue Instanz erzeugt. Veröffentlichte Modelleigenschaften steuern den angezeigten Status und das Ergebnis. Dokumentation zu StateObject
Das Modell und die Task-Closure sind ausdrücklich mit @MainActor markiert. Apple definiert den Executor dieses Actors als gleichwertig mit der Main Dispatch Queue. In dieser Implementierung finden Planung, Abbruch und Ergebniszuweisung daher auf demselben Actor statt. Das Schlafen hält den Task an; der kurze arithmetische Ausdruck wird nach der Wartezeit auf diesem Actor ausgeführt. Dokumentation zu MainActor
Wie Eingabe- und Ergebnisregeln veraltete Vorschauen verhindern
Der Abbruch erfolgt vor der Validierung. Wenn der Nutzer eine gültige Zahl in ein leeres Feld oder einen ungültigen Wert ändert, wird die ältere ausstehende Berechnung dennoch abgebrochen. Ein vorzeitiges Zurückkehren bei der Validierung lässt die ältere Verzögerung nicht weiterlaufen und ein sichtbares Ergebnis erzeugen.
Das Programm löscht result bei jeder Änderung. Dies ist eine bewusste Anzeigeregel: Das Ergebnis verschwindet, während ein neuer Wert wartet, sodass die Oberfläche niemals eine alte Summe als Antwort auf den aktuellen Text darstellt. Ein alternatives Design könnte das Ergebnis mit einem klaren Hinweis auf das vorherige Ergebnis beibehalten, würde aber andere Anzeigeregeln erfordern.
Foundation entfernt mit trimmingCharacters(in:) passende Zeichen an beiden Enden einer Zeichenfolge. Durch die Verwendung von .whitespacesAndNewlines wird eine Eingabe wie 3 zu 3; Zeichen im Inneren bleiben unverändert. Dokumentation zum Bereinigen von Zeichenfolgen in Foundation
Der Integer-Parser von Swift verwendet standardmäßig die Basis 10, akzeptiert ein optionales Vorzeichen am Anfang und gibt bei ungültigen oder nicht darstellbaren Werten nil zurück. Deshalb werden hier +3 und 003 akzeptiert, während 3.5, 1,000 und 3 0 abgelehnt werden. Ein geparster negativer Wert wird durch die separate Bereichsprüfung abgelehnt. Dokumentation zu Int.init
Die Obergrenze begrenzt auch die Rechenwerte. Bei 1000 Ebenen beträgt der Zähler 2,003,001,000 und die Summe 333,833,500. Alle zulässigen Eingaben bleiben innerhalb dieser nicht negativen Zwischenwertgrenzen. Bei einer Erhöhung des Grenzwerts müssen sowohl die Zwischenmultiplikation als auch das Endergebnis auf Überläufe geprüft werden; eine anschließende Division durch sechs kann einen bereits eingetretenen Überlauf nicht beheben.
Jeder verzögerte Task erfasst den für diese Änderung validierten Wert layers. Nach dem Schlafen liest er das Feld nicht erneut aus. Sobald der Task seine Abbruchprüfung bestanden hat, gibt es kein weiteres await, bevor er das Ergebnis zuweist. Ein weiterer Planungsvorgang auf dem Main Actor kann sich nicht in diesen synchronen Abschnitt aus Berechnung und Zuweisung einschieben. Apples Sprachleitfaden erläutert die Actor-Isolation und ausdrücklich markierte Unterbrechungspunkte. Nebenläufigkeit — Die Swift-Programmiersprache
Diese Überlegung setzt voraus, dass die Berechnung synchron und sehr klein bleibt. Wenn du später einen asynchronen Vorgang einfügst, prüfe nach dessen Rückkehr erneut, ob ein Abbruch vorliegt und ob die Anfrage noch aktuell ist. Eine Revisionskennung kann aufeinanderfolgende Anfragen unterscheiden, auch wenn der Nutzer zu einem früheren Eingabewert zurückkehrt.
Verhalten mit synthetischen Eingaben prüfen
Erstelle und starte die App und führe dann diese Abnahmesequenz aus. Die erwarteten Ergebnisse ergeben sich aus den Regeln des Programms; die genauen Aufwachzeiten hängen von der Planung ab.
| Aktion | Erwartetes Verhalten |
|---|---|
Fenster mit der anfänglichen Eingabe 3 öffnen |
Wartestatus anzeigen, dann Total tiles: 14. |
Eingabe durch 4 ersetzen |
Vorherige Summe löschen, dann Total tiles: 30 anzeigen. |
1 und dann 10 eingeben, bevor die erste Verzögerung abläuft |
Verzögerung neu starten; der stabile Wert ergibt Total tiles: 385. |
4 eingeben und vor Ablauf der Verzögerung löschen |
Enter a layer count. und keine Summe anzeigen. |
3.5 oder 1001 eingeben |
Bereichsanweisung sofort und keine Summe anzeigen. |
0 eingeben |
Nach der Verzögerung Total tiles: 0 anzeigen. |
1000 eingeben |
Nach der Verzögerung Total tiles: 333833500 anzeigen. |
Führe für Abbruchprüfungen die nächste Änderung mit genügend Abstand vor Ablauf der Verzögerung aus. An der Zeitgrenze könnte ein älteres Ergebnis bereits abgeschlossen sein, bevor die nächste Änderung verarbeitet wird. Die nächste Änderung löscht es trotzdem und startet die neue Anfrage. Eine Verzögerung ist kein genauer Ausführungstermin; Apples Sprachleitfaden beschreibt, dass sleep den Task mindestens für das angeforderte Intervall anhält. Dokumentation zur Swift-Nebenläufigkeit
Die obigen Rechenbeispiele wurden unabhängig mit Ganzzahlberechnungen geprüft. In der Rechercheumgebung war kein Swift-Compiler verfügbar, daher wurde das macOS-Programm dort geprüft, aber nicht kompiliert oder ausgeführt.
Den Abbruch an die Lebensdauer der Ansicht koppeln
Der Handler für das Verschwinden der Ansicht ruft stop() ausdrücklich auf. Apple dokumentiert, dass onDisappear ausgeführt wird, nachdem die Ansicht verschwunden ist; der genaue Zeitpunkt hängt vom Ansichtstyp ab. Betrachte ihn als Bereinigungsroutine der Ansicht und nicht als exakten Termin für ein Fensterereignis. Der Handler für das Erscheinen plant die aktuelle Eingabe bei jedem Erscheinen der Ansicht erneut ein. SwiftUI-Dokumentation zu onDisappear
Der Abbruch-Catch kehrt bewusst zurück, ohne den gemeinsamen Status zu ändern oder das gespeicherte Handle zu löschen. Ein älterer abgebrochener Task könnte fortgesetzt werden, nachdem ein neuerer Task gespeichert wurde; eine bedingungslose Bereinigung durch den älteren Task könnte dann das neuere Handle löschen. In diesem Programm löschen die Methoden zum Planen und Beenden abgebrochene Handles, und nur ein erfolgreich abgeschlossener Task löscht sein eigenes aktives Handle.
Entprellung steuert, wann die Arbeit beginnt. Sie macht eine lange synchrone Berechnung weder reaktionsschnell, noch kann sie ein bereits zugewiesenes Ergebnis rückgängig machen. Für diese begrenzte Formel ist das passende Muster kompakt: das vorherige Handle abbrechen, eine erfasste Eingabe validieren, während der Ruhezeit warten, bei Abbruch beenden und die kurze Berechnung auf dem Main Actor übernehmen. So erhält die Mac-Oberfläche eine vorhersehbare Vorschau für stabile Eingaben.
Quellen
- 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.