För en liten Mac-redigerare för former som arbetar helt i minnet bör du börja med ögonblicksbilder av värdetillståndet. Använd inversa kommandon för frekventa, avgränsade redigeringar när den mindre nyttolasten motiverar den extra logiken. Båda metoderna behöver en enda stabil ägare och en tydlig gräns för när varje användaråtgärd sparas.
Den här guiden behandlar en uppgift: att implementera förutsägbar ångring och upprepning när syntetiska former flyttas, tas bort och ordnas om i en egen Swift-redigerare. Beslutsramverket nedan är en teknisk rekommendation baserad på Apples API:er; Apple föreskriver inte ett universellt val mellan ögonblicksbilder och kommandon.
Vad ska varje ångraåtgärd komma ihåg?
En ögonblicksbild registrerar det föregående redigerbara tillståndet, till exempel en ordnad array med former. Ångring återställer den arrayen. Ett inverskommando registrerar den åtgärd som krävs för att upphäva en viss redigering, till exempel att återställa en forms mittpunkt till dess tidigare koordinater.
Båda representationerna fungerar med Foundations UndoManager. Den registrerar åtgärder för ett objekt eller dess ägare; den observerar inte automatiskt godtyckliga tilldelningar till din modell. Dina ändringsmetoder måste registrera lämpligt återställningsarbete. UndoManager
Definiera det redigerbara tillståndet innan du väljer hur historiken ska representeras. I den här redigeraren omfattar det form-ID:n, koordinater och arrayordning. Låt hovringsindikatorer och förhandsvisningar av dragningar ligga utanför. Bestäm uttryckligen om markeringen ska ingå i historiken: om markeringen återställs kan det bli lättare att förstå en borttagning, men då ändrar ångring också den aktuella markeringen.
Använd den här beslutstabellen för varje åtgärd i stället för att kräva att hela redigeraren använder en och samma representation:
| Åtgärd eller villkor | Föreslagen representation | Information att spara |
|---|---|---|
| Liten arbetsyta; flera egenskaper ändras samtidigt | Ögonblicksbild | Hela det föregående redigerbara värdet |
| Flytta en form | Invers setter | Stabilt form-ID och tidigare mittpunkt |
| Ta bort en form | Ögonblicksbild till att börja med; inverskommando vid behov | För ett inverskommando: hela den borttagna formen och dess ursprungliga position |
| Ordna om flera former | Ögonblicksbild, såvida inversen inte är enkel | Tidigare ordning och berörda egenskaper |
| Uppmätt minneskostnad för historiken är för hög | Riktade inversa kommandon | Allt som krävs för exakt återställning |
Den användbara frågan är: kan du räkna upp allt som inversen måste återställa? Om borttagning av en form också ändrar markering och ordning räcker det inte att bara spara det borttagna ID:t. En ögonblicksbild är ofta enklare att granska när åtgärden påverkar flera relaterade värden.
När är en ögonblicksbild faktiskt fristående?
Swift-strukturer och arrayer har värdesemantik. En array med former som enbart består av värdetyper kan därför representera ett tidigare tillstånd oberoende av senare ändringar. En array med klassinstanser bevarar däremot referenserna till instanserna när den kopieras; om en instans ändras kan även den förmodade ögonblicksbilden visa ändringen. Att omsluta referenserna i en struktur löser inte problemet. Value And Reference Types In Swift
För en liten redigerare använder du enkla värdedata i historiken: identifierare, koordinater, dimensioner och andra redigerbara attribut. Låt vyer och ändringsbara renderingsobjekt ligga utanför ögonblicksbilden. Om den aktiva modellen använder klasser skapar du en separat värderepresentation som innehåller alla attribut som behövs för att återställa den.
Dra inte slutsatser om en fast minneskostnad utifrån ordet ”ögonblicksbild”. Mät hur mycket historik som behålls och redigeringsfördröjningen med representativa syntetiska arbetsytor. Som ett illustrativt exempel på nyttolaststorlek motsvarar 100 poster som var och en behåller 1 000 formposter 100 000 förekomster av poster; 100 inversposter för enstaka former behåller bara 100 tidigare mittpunkter plus identifierare. Dessa antal motsvarar inte uppmätta byte och är inget påstående om hur Swift allokerar lagring.
Bestäm historikdjupet medvetet. Apple dokumenterar att levelsOfUndo begränsar antalet grupper på toppnivå och att standardvärdet 0 innebär obegränsat antal grupper. En gruppgräns är användbar, men den anger ingen minnesbudget i byte: en ögonblicksbild av en omgångsredigering och en redigering av en enda koordinat kan behålla mycket olika datamängder. levelsOfUndo
Hur bevarar ögonblicksbilder och inversa kommandon möjligheten att göra om?
Återställningsmetoden bör registrera den motsatta återställningen innan modellen ändras. När ångring kör den metoden möjliggör registreringen att åtgärden görs om. För en ögonblicksbild sparar du det tillstånd som är installerat för tillfället innan du återställer det föregående tillståndet. För ett positionskommando sparar du den aktuella mittpunkten innan du installerar den sparade mittpunkten.
Apples arkiverade guide till ångringsarkitektur förklarar uttryckligen detta mönster med ömsesidig registrering: en setter som anropas under ångring registrerar värdet som behövs för att göra om åtgärden. Exemplen använder Objective-C; det fullständiga Swift-exemplet nedan använder det aktuella API:t med closures. Registering Undo Operations
Undertryck inte registreringen enbart för att isUndoing är true. I den här utformningen skulle det ta bort registreringen som skapar möjligheten att göra om. Undvik också att spela upp en ny relativ förflyttning när du återställer en position: om du lagrar den exakta tidigare mittpunkten blir den avsedda återställningen tydlig.
Ett fullständigt exempel på modellnivå
Det här fristående macOS-programmet kombinerar en ögonblicksbildsbaserad borttagning med en invers setter för position. Det använder endast syntetiska värden i minnet och Foundation, utan beroenden till beständig lagring eller gränssnitt. Spara det som EditorUndo.swift och kompilera med en aktuell Swift 6-verktygskedja och macOS-SDK:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Programmet äger avsiktligt en särskild hanterare och inaktiverar händelsegruppering så att dess påståenden inte är beroende av en körande händelseslinga i appen. Varje inledande redigering får en uttrycklig grupp. Återställningsmetoderna öppnar inga ytterligare grupper under ångring eller upprepning. Apple kräver att grupper uttryckligen stängs före ångring när automatisk gruppering är inaktiverad. 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")
}
}
Förväntad utdata är Undo and redo checks passed. Exemplet granskades men kördes inte här eftersom det saknades en Swift-kompilator i forskningsmiljön. Det visar historikens kärna; ett SwiftUI-gränssnitt behöver också en observerbar modell eller adapter för att uppdatera sina vyer.
Closuren använder parametern target i stället för att fånga redigeraren direkt. Apple dokumenterar att registreringsmålet måste vara en referenstyp, att hanteraren behåller en oägd referens till det och att användning av closure-parametern undviker en behållningscykel via ett infångat mål. registerUndo(withTarget:handler:)
Förvillkoren synliggör felaktiga modellantaganden i stället för att tyst utelämna en ångraåtgärd. Gränsen för produktionskommandon bör avvisa en ogiltig redigering före registrering och visa ett lämpligt fel på programnivå. Saknade ID:n vid uppspelning av historiken tyder på att något har ändrat modellen utanför den förväntade historiksekvensen.
Hur blir ett drag en enda ångringsbar åtgärd?
Ett drag ger många uppdateringar, medan användaren förväntar sig att en avslutad förflyttning ska ångras som en enda åtgärd. Apples automatiska gruppering följer genomgångar av körslingan och kan därför inte ses som en definition av en hel gest. Uttryckliga grupper kan slå samman registreringar, men gränserna måste fortfarande hanteras medvetet. groupsByEvent, beginUndoGrouping()
Använd en sekvens med förhandsvisning och verkställande i den här redigeraren:
- När dragningen börjar sparar du mål-ID:t och den verkställda startmittpunkten som gestens utgångspunkt.
- Använd
@GestureStateochupdating(_:body:)för den tillfälliga förflyttningen. Rendera formen vid dess utgångspunkt plus förflyttningen utan att ändra de verkställda objekten. - Vid lyckad
onEnded(_:)beräknar du den slutliga mittpunkten utifrån gestens slutvärde och anropar modellens ångringsbara setter en gång. - Hoppa över registreringen om den nya mittpunkten är densamma som den verkställda mittpunkten.
- Vid avbrytning kastar du bort förhandsvisningen och behåller den verkställda modellen.
Den här sekvensen följer SwiftUI:s dokumenterade skillnad: gesttillståndet återställs när en gest avslutas eller avbryts, medan onEnded(_:) körs endast när gesten slutförs. Därför kan städning som även måste ske efter avbrytning inte enbart förlita sig på onEnded(_:). Adding interactivity with gestures
Håll redigerarens verkställda tillstånd stabilt under förhandsvisningen. En enkel princip är att inaktivera andra ändringar på arbetsytan och ångring där medan ett drag pågår, och sedan återaktivera dem när det slutförs eller avbryts. Annars måste du definiera hur en mellanliggande redigering gör den sparade utgångspunkten ogiltig innan du godkänner dragändringen.
Om en annan del av redigeraren måste observera kontinuerligt uppdaterade verkställda koordinater kanske rendering med enbart förhandsvisning inte passar. Implementera i så fall en uttrycklig gesttransaktion: spara starttillståndet, tillämpa mellanliggande uppdateringar utan separata historikposter och registrera en enda återställning vid lyckat slutförande. Vid avbrytning måste starttillståndet återställas. Undvik att lämna en delad ångringsgrupp öppen över godtyckliga återanrop eller asynkront arbete.
Vem ska äga modellen och ångringshanteraren?
Behåll en stabil redigerarägare för varje fristående redigerbar arbetsyta. Den äger de verkställda objekten och är målet för historikåtgärderna. Kör både modelländringar och interaktioner med ångringshanteraren på huvudaktören; Apples aktuella dokumentation markerar UndoManager och dess hanterare för closures som isolerade till huvudaktören. UndoManager
I SwiftUI läser du vyns hanterare med @Environment(\.undoManager). Skicka den vidare genom ett stabilt redigerings- eller kommandolager i stället för att skapa en ny hanterare vid varje utvärdering av vyn. Miljövärdet är valfritt: Apple dokumenterar att kontexter som inte stöds returnerar nil och tillåter att ångringsregistrering utelämnas där. Redigering kan fortfarande utföras, men den sökvägen får inte ange att ångring är tillgänglig. EnvironmentValues.undoManager
För en AppKit-arbetsyta hämtar du hanteraren genom lämplig responder. Grundimplementationen av NSResponder.undoManager frågar nästa responder. Därför behöver en privat skapad hanterare integreras uttryckligen i arbetsytans responderkontext om standardåtgärderna i menyn ska nå den. Det räcker inte att enbart lagra den i modellen för att upprätta kopplingen. NSResponder.undoManager
Grupperingskonfigurationen i det fristående exemplet gäller dess särskilda hanterare. Kopiera inte den konfigurationen till en hanterare från miljön om du inte äger grupperingspolicyn för alla som använder den. I ett gränssnitt registrerar du den avslutade ändringen på arbetsytan med kontextens befintliga händelsepolicy och grupperar flera synkrona delåtgärder när de utgör en enda åtgärd.
Innan du gör dig av med ett redigeringsmål tar du bort dess väntande registreringar vid en kontrollerad livscykelgräns. Apple tillhandahåller removeAllActions(withTarget:) för att ta bort ånger- och upprepningsåtgärder som riktats till målet. Om du rensar hela hanteraren påverkas även andra deltagare när den delas. removeAllActions(withTarget:)
Vad bör du verifiera innan du väljer den slutliga utformningen?
Testa en kort sekvens som passerar mellan olika representationer: flytta en form, ta bort en annan, ångra båda och gör sedan om båda. Kontrollera värden, ID:n och ordning efter varje steg. Lägg till ett avbrutet drag, ett drag som slutar i startpositionen och två oberoende redigeringsfönster. Varje fall testar en egen gräns: återställningsdata, gestens slutförande, hantering av åtgärder utan ändring eller ägarskap.
Behandla ett massutbyte av arbetsytan som en uttrycklig åtgärd. Gör den antingen ångringsbar med tillräckliga återställningsdata eller skapa en ny utgångspunkt efter att avsiktligt ha rensat inaktuell historik. En ögonblicksbild som utelämnar redigerbara fält och ett inverskommando som inte hittar sitt mål är båda ofullständiga implementationer.
Börja med ögonblicksbilder för en liten värdebaserad arbetsyta, behåll en enda stabil ägare och verkställ varje gest en gång. Inför inversa kommandon där återställningsdata är tydliga och mätningar motiverar den extra logiken. Då kan historikrepresentationen ändras utan att betydelsen av en användaråtgärd förändras.
Källor
- 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