Begin bij een kleine Mac-vormeditor die volledig in het geheugen werkt met snapshots van waardestatussen. Gebruik inverse opdrachten voor frequente, afgebakende bewerkingen wanneer de kleinere payload de extra logica rechtvaardigt. Beide benaderingen vereisen één stabiele eigenaar en een duidelijk commitmoment voor elke gebruikersactie.
Deze gids behandelt één taak: voorspelbaar ongedaan maken en opnieuw uitvoeren implementeren voor het verplaatsen, verwijderen en herschikken van synthetische vormen in je eigen Swift-editor. Het besliskader hieronder is een aanbeveling op basis van technische overwegingen en afgeleid van Apple-API's; Apple schrijft geen universele keuze tussen snapshots en opdrachten voor.
Wat moet elke bewerking om ongedaan te maken onthouden?
Een snapshot legt de vorige bewerkbare status vast, zoals een geordende array met vormen. Ongedaan maken herstelt die array. Een inverse opdracht legt de bewerking vast die nodig is om een specifieke wijziging terug te draaien, zoals het middelpunt van één vorm terugzetten op de vorige coördinaten.
Beide representaties werken via Foundation's UndoManager. Deze registreert bewerkingen voor een object of diens eigenaar; willekeurige toewijzingen aan je model worden niet automatisch waargenomen. Je mutatiemethoden moeten de juiste herstelbewerking registreren. UndoManager
Bepaal de bewerkbare status voordat je de representatie van de geschiedenis kiest. Voor deze editor omvat die vorm-ID's, coördinaten en de volgorde in de array. Houd zweefindicatoren en sleepvoorbeelden erbuiten. Beslis expliciet of de selectie deel uitmaakt van de geschiedenis: de selectie herstellen kan het verwijderen begrijpelijker maken, maar betekent ook dat ongedaan maken de huidige selectie wijzigt.
Gebruik deze beslistabel per bewerking, in plaats van de hele editor tot één representatie te verplichten:
| Bewerking of voorwaarde | Aanbevolen representatie | Te bewaren informatie |
|---|---|---|
| Klein canvas; meerdere eigenschappen veranderen tegelijk | Snapshot | Volledige vorige bewerkbare waarde |
| Eén vorm verplaatsen | Inverse setter | Stabiele vorm-ID en vorig middelpunt |
| Eén vorm verwijderen | Aanvankelijk snapshot; zo nodig inverse opdracht | Voor een inverse: volledige verwijderde vorm en oorspronkelijke positie |
| Meerdere vormen herschikken | Snapshot, tenzij de inverse eenvoudig is | Vorige volgorde en alle gewijzigde eigenschappen |
| Gemeten geheugenkosten van de geschiedenis zijn te hoog | Gerichte inverse opdrachten | Alles wat nodig is voor exact herstel |
De nuttige vraag is: kun je alles opsommen wat de inverse moet herstellen? Als het verwijderen van een vorm ook de selectie en volgorde wijzigt, is alleen de verwijderde ID onthouden niet voldoende. Een snapshot is vaak eenvoudiger te controleren wanneer de bewerking meerdere samenhangende waarden beïnvloedt.
Wanneer is een snapshot echt onafhankelijk?
Swift-structuren en arrays hebben waardesemantiek. Een array met vormen die volledig uit waardetypen bestaat, kan daarom een eerdere status onafhankelijk van latere wijzigingen weergeven. Een array met klasse-instanties kopieert echter de verwijzingen naar die instanties; een instantie wijzigen kan ook veranderen wat je vermeende snapshot laat zien. Die verwijzingen in een structuur verpakken lost dat probleem niet op. Value And Reference Types In Swift
Gebruik voor een kleine editor gewone waardedata voor de geschiedenis: ID's, coördinaten, afmetingen en andere bewerkbare kenmerken. Houd weergaven en veranderlijke renderobjecten buiten de snapshot. Als het live model klassen gebruikt, maak dan een afzonderlijke waarderepresentatie met alle kenmerken die nodig zijn om het te herstellen.
Leid geen vaste geheugenkosten af uit het woord ‘snapshot’. Meet de bewaarde geschiedenis en bewerkingslatentie met representatieve synthetische canvassen. Ter illustratie van de payload: 100 items die elk 1.000 vormrecords bewaren, vertegenwoordigen 100.000 recordvermeldingen; 100 inverse items voor één vorm bewaren slechts 100 eerdere middelpunten plus ID's. Die aantallen zijn geen gemeten bytes en zeggen niets over de geheugenallocatie van Swift.
Stel bewust een maximale diepte voor de geschiedenis in. Apple documenteert dat levelsOfUndo het aantal groepen op het hoogste niveau beperkt en dat de standaardwaarde nul betekent dat het aantal groepen onbeperkt is. Een groepslimiet is nuttig, maar vormt geen bytebudget: een batchsnapshot en een wijziging van één coördinaat kunnen heel verschillende hoeveelheden gegevens bewaren. levelsOfUndo
Hoe zorgen snapshots en inverse opdrachten voor opnieuw uitvoeren?
De herstelmethode moet de tegenovergestelde herstelbewerking registreren voordat het model wordt gewijzigd. Wanneer ongedaan maken die methode uitvoert, levert de registratie de bewerking voor opnieuw uitvoeren. Leg bij een snapshot eerst de huidige status vast voordat je de vorige status herstelt. Leg bij een positieopdracht eerst het huidige middelpunt vast voordat je het bewaarde middelpunt instelt.
Apple's gearchiveerde gids over de undo-architectuur legt dit wederkerige registratiepatroon expliciet uit: een setter die tijdens ongedaan maken wordt aangeroepen, registreert de waarde die nodig is om opnieuw uit te voeren. De voorbeelden gebruiken Objective-C; het volledige Swift-voorbeeld hieronder gebruikt de huidige closure-API. Registering Undo Operations
Onderdruk registraties niet alleen omdat isUndoing waar is. In dit ontwerp zou dat de registratie verwijderen die opnieuw uitvoeren mogelijk maakt. Voer ook geen nieuwe relatieve verplaatsing uit tijdens het herstellen van een positie: de exacte vorige coördinaat opslaan maakt het beoogde herstel expliciet.
Een volledig voorbeeld op modelniveau
Dit zelfstandige macOS-programma combineert verwijderen op basis van snapshots met een inverse setter voor posities. Het gebruikt alleen synthetische waarden in het geheugen en Foundation, zonder afhankelijkheden van opslag of UI. Sla het op als EditorUndo.swift en compileer het met een actuele Swift 6-toolchain en macOS SDK:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Het programma gebruikt bewust een eigen manager en schakelt gebeurtenisgroepering uit, zodat de beweringen niet afhankelijk zijn van een actieve app-eventloop. Elke initiële bewerking krijgt een expliciete groep. De herstelmethoden openen tijdens ongedaan maken of opnieuw uitvoeren geen extra groepen. Apple vereist dat groepen expliciet worden gesloten voordat je ongedaan maakt wanneer automatische groepering is uitgeschakeld. groupsByEvent
import Foundation
struct Center: Equatable {
var x: Double
var y: Double
}
struct Item: Equatable {
let id: Int
var center: Center
}
@MainActor
final class Editor {
private(set) var items: [Item]
let history: UndoManager
init(items: [Item], history: UndoManager) {
precondition(Set(items.map(\.id)).count == items.count)
self.items = items
self.history = history
}
func replaceItems(_ next: [Item], name: String) {
precondition(Set(next.map(\.id)).count == next.count)
guard next != items else { return }
let previous = items
history.registerUndo(withTarget: self) { target in
target.replaceItems(previous, name: name)
}
items = next
history.setActionName(name)
}
func setCenter(id: Int, to next: Center) {
guard let index = items.firstIndex(where: { $0.id == id }) else {
preconditionFailure("History refers to a missing item")
}
let previous = items[index].center
guard previous != next else { return }
history.registerUndo(withTarget: self) { target in
target.setCenter(id: id, to: previous)
}
items[index].center = next
history.setActionName("Move Shape")
}
}
@main
struct Demo {
@MainActor
static func main() {
let history = UndoManager()
history.groupsByEvent = false
history.levelsOfUndo = 100
let original = [
Item(id: 1, center: Center(x: 20, y: 30)),
Item(id: 2, center: Center(x: 80, y: 30))
]
let editor = Editor(items: original, history: history)
func edit(_ operation: () -> Void) {
history.beginUndoGrouping()
defer { history.endUndoGrouping() }
operation()
}
edit {
editor.setCenter(id: 1, to: Center(x: 50, y: 40))
}
let moved = editor.items
edit {
editor.replaceItems(
editor.items.filter { $0.id != 2 },
name: "Delete Shape"
)
}
let deleted = editor.items
history.undo()
precondition(editor.items == moved)
history.undo()
precondition(editor.items == original)
history.redo()
precondition(editor.items == moved)
history.redo()
precondition(editor.items == deleted)
print("Undo and redo checks passed")
}
}
De verwachte uitvoer is Undo and redo checks passed. Het voorbeeld is nagekeken, maar hier niet uitgevoerd omdat er in de onderzoeksomgeving geen Swift-compiler beschikbaar was. Het laat de kern van de geschiedenis zien; een SwiftUI-interface heeft ook een observeerbaar model of een adapter nodig om de weergaven bij te werken.
De closure gebruikt de parameter target in plaats van de editor rechtstreeks vast te leggen. Apple documenteert dat het registratiedoel een referentietype moet zijn, dat de manager een niet-eigen referentie naar dat doel bewaart en dat de closureparameter gebruiken een retain cycle via een vastgelegd doel voorkomt. registerUndo(withTarget:handler:)
De precondities maken onjuiste modelaannames zichtbaar in plaats van een bewerking om ongedaan te maken stilzwijgend te laten vallen. Een productiegrens voor opdrachten moet een ongeldige bewerking afwijzen voordat die wordt geregistreerd en een passende fout op applicatieniveau tonen. Ontbrekende ID's tijdens het afspelen van de geschiedenis wijzen erop dat iets het model heeft gewijzigd buiten de verwachte geschiedenisvolgorde om.
Hoe wordt een sleep één actie die ongedaan kan worden gemaakt?
Een sleep levert veel updates op, terwijl de gebruiker verwacht dat één voltooide verplaatsing als geheel ongedaan wordt gemaakt. Apple groepeert automatisch op basis van passages door de runloop; dat kan dus niet gelden als definitie van een volledige beweging. Expliciete groepen kunnen registraties combineren, maar de grenzen moeten nog steeds zorgvuldig worden beheerd. groupsByEvent, beginUndoGrouping()
Gebruik voor deze editor een reeks met voorbeeldweergave en commit:
- Bewaar bij het begin van de sleep de doel-ID en het vastgelegde beginmiddelpunt als uitgangspunt voor het gebaar.
- Gebruik
@GestureStateenupdating(_:body:)voor de tijdelijke verplaatsing. Geef de vorm weer op het uitgangspunt plus die verplaatsing, zonder de vastgelegde items te wijzigen. - Bereken bij een geslaagde
onEnded(_:)het eindmiddelpunt op basis van de uiteindelijke gebarenwaarde en roep de undoable setter van het model één keer aan. - Sla registratie over als het resulterende middelpunt gelijk is aan het vastgelegde middelpunt.
- Gooi bij annulering de voorbeeldweergave weg en behoud het vastgelegde model.
Deze reeks volgt het gedocumenteerde onderscheid in SwiftUI: de gebarenstatus wordt gereset wanneer een gebaar eindigt of wordt geannuleerd, terwijl onEnded(_:) alleen wordt uitgevoerd na succesvolle voltooiing. Opruimwerk dat ook na annulering moet plaatsvinden, kan daarom niet uitsluitend afhangen van onEnded(_:). Adding interactivity with gestures
Houd tijdens die voorbeeldweergave de vastgelegde status van de editor stabiel. Een eenvoudig beleid is om andere canvaswijzigingen en ongedaan maken op het canvas uit te schakelen zolang een sleep actief is, en ze na voltooiing of annulering weer in te schakelen. Definieer anders hoe een tussentijdse bewerking het opgeslagen uitgangspunt ongeldig maakt voordat een sleepcommit wordt geaccepteerd.
Als een ander deel van de editor voortdurend bijgewerkte vastgelegde coördinaten moet kunnen waarnemen, is alleen weergeven van een voorbeeld mogelijk niet geschikt. Implementeer in dat geval een expliciete gebarentransactie: sla de beginstatus op, pas tussentijdse updates toe zonder afzonderlijke geschiedenisitems en registreer bij succes één herstelbewerking. Bij annulering moet de beginstatus worden hersteld. Laat geen gedeelde undo-groep openstaan over willekeurige callbacks of asynchroon werk.
Wie moet eigenaar zijn van het model en de undo-manager?
Houd voor elk onafhankelijk bewerkbaar canvas één stabiele eigenaar van de editor aan. Die beheert de vastgelegde items en is het doel van de geschiedenisbewerkingen. Voer zowel modelwijzigingen als undo-bewerkingen uit op de main actor; in de actuele Apple-documentatie zijn UndoManager en de closure-handler geïsoleerd tot de main actor. UndoManager
Lees in SwiftUI de manager van de weergave met @Environment(\.undoManager). Geef die manager door via een stabiele editor- of opdrachtlaag, in plaats van telkens tijdens het evalueren van de weergave een nieuwe manager aan te maken. De omgevingswaarde is optioneel: Apple documenteert dat niet-ondersteunde contexten nil teruggeven en staat toe dat undo-registratie daar wordt overgeslagen. Bewerken kan nog steeds, maar in dat geval mag niet worden aangegeven dat ongedaan maken beschikbaar is. EnvironmentValues.undoManager
Haal voor een AppKit-canvas de manager op via de toepasselijke responder. De implementatie van NSResponder.undoManager in de basisklasse vraagt het volgende responder-object. Een zelf aangemaakte manager vereist daarom expliciete integratie met de respondercontext van het canvas als standaardmenuacties hem moeten kunnen bereiken. Hem alleen in het model opslaan, brengt die verbinding niet tot stand. NSResponder.undoManager
De configuratie voor groepering in het zelfstandige voorbeeld hoort bij de eigen manager ervan. Pas die configuratie niet toe op een via de omgeving verkregen manager, tenzij je eigenaar bent van het groeperingsbeleid voor alle deelnemers. Registreer in een UI de voltooide canvasbewerking volgens het bestaande gebeurtenisbeleid van de context; groepeer meerdere synchrone deelbewerkingen als ze samen één actie vormen.
Verwijder, voordat je een editordoel opruimt, de bijbehorende resterende registraties op een gecontroleerd moment in de levenscyclus. Apple biedt removeAllActions(withTarget:) om undo- en redo-bewerkingen te verwijderen die op dat doel gericht zijn. De volledige manager wissen zou ook andere deelnemers treffen als de manager wordt gedeeld. removeAllActions(withTarget:)
Wat moet je controleren voordat je het definitieve ontwerp kiest?
Doorloop een korte reeks die de grenzen tussen representaties overschrijdt: verplaats een vorm, verwijder een andere, maak beide bewerkingen ongedaan en voer ze daarna opnieuw uit. Controleer na elke stap de waarden, ID's en volgorde. Voeg een geannuleerde sleep toe, een sleep die eindigt op de beginpositie en twee onafhankelijke editorvensters. Elk geval test een andere grens: herstelgegevens, voltooiing van gebaren, afhandeling van bewerkingen zonder wijzigingen of eigenaarschap.
Behandel een volledige vervanging van het canvas als een expliciete bewerking. Maak die ongedaan kunnen worden met voldoende herstelgegevens, of stel een nieuwe basisstatus in nadat je bewust verouderde geschiedenis hebt gewist. Zowel een snapshot waarin bewerkbare velden ontbreken als een inverse opdracht die zijn doel niet kan vinden, is een onvolledige implementatie.
Begin met snapshots voor een klein waardegericht canvas, behoud één stabiele eigenaar en commit elk gebaar één keer. Voeg inverse opdrachten toe waar de herstelgegevens duidelijk zijn en metingen de extra logica rechtvaardigen. Zo kan de representatie van de geschiedenis veranderen zonder dat de betekenis van één gebruikersactie verandert.
Bronnen
- UndoManager — Apple
- registerUndo(withTarget:handler:) — Apple
- groupsByEvent — Apple
- beginUndoGrouping() — Apple
- levelsOfUndo — Apple
- EnvironmentValues.undoManager — Apple
- NSResponder.undoManager — Apple
- Adding interactivity with gestures — Apple
- removeAllActions(withTarget:) — Apple
- Registering Undo Operations — Apple, Documentation Archive
- Value And Reference Types In Swift — Swift.org