W przypadku niewielkiego edytora kształtów na Maca, działającego w pamięci, zacznij od migawek stanu opartego na wartościach. Używaj poleceń odwrotnych przy częstych, wąsko zakrojonych edycjach, gdy mniejszy rozmiar danych uzasadnia dodatkową logikę. Oba podejścia wymagają jednego stabilnego właściciela i wyraźnej granicy zatwierdzenia każdej czynności użytkownika.
Ten poradnik dotyczy jednego zadania: implementacji przewidywalnego cofania i ponawiania przesuwania, usuwania i zmiany kolejności syntetycznych kształtów we własnym edytorze Swift. Poniższe ramy decyzyjne to zalecenie inżynierskie opracowane na podstawie interfejsów API Apple; Apple nie zaleca uniwersalnego wyboru między migawkami a poleceniami.
Co powinna zapamiętywać każda operacja cofania?
Migawka zapisuje poprzedni stan edytowalny, na przykład uporządkowaną tablicę kształtów. Cofnięcie przywraca tę tablicę. Polecenie odwrotne zapisuje operację potrzebną do odwrócenia konkretnej edycji, na przykład ustawienie środka jednego kształtu z powrotem na poprzednich współrzędnych.
Obie reprezentacje działają za pośrednictwem UndoManager z Foundation. Rejestruje on operacje dla obiektu lub jego właściciela; nie obserwuje automatycznie dowolnych przypisań do modelu. Metody modyfikujące muszą rejestrować odpowiednie działania przywracające. UndoManager
Zdefiniuj stan edytowalny, zanim wybierzesz jego reprezentację w historii. W tym edytorze obejmuje on identyfikatory kształtów, współrzędne i kolejność w tablicy. Wskaźniki najechania i podglądy przeciągania trzymaj poza nim. Zdecyduj wprost, czy historia ma obejmować zaznaczenie: przywrócenie zaznaczenia może ułatwić zrozumienie usunięcia, ale oznacza też, że cofnięcie zmienia bieżące zaznaczenie.
Dla każdej operacji korzystaj z poniższej tabeli decyzyjnej, zamiast wymagać, by cały edytor używał jednej reprezentacji:
| Operacja lub warunek | Sugerowana reprezentacja | Informacje do zachowania |
|---|---|---|
| Małe płótno; kilka właściwości zmienia się jednocześnie | Migawka | Pełna poprzednia wartość edytowalna |
| Przesunięcie jednego kształtu | Ustawienie odwrotne | Stabilny identyfikator kształtu i poprzedni środek |
| Usunięcie jednego kształtu | Początkowo migawka; w razie potrzeby polecenie odwrotne | Dla polecenia odwrotnego: cały usunięty kształt i jego pierwotna pozycja |
| Zmiana kolejności wielu kształtów | Migawka, chyba że polecenie odwrotne jest proste | Poprzednia kolejność i wszelkie zmienione właściwości |
| Zmierzony koszt pamięci historii jest nadmierny | Ukierunkowane polecenia odwrotne | Wszystko, co jest potrzebne do dokładnego przywrócenia |
Warto zapytać: czy potrafisz wymienić wszystko, co musi przywrócić polecenie odwrotne? Jeśli usunięcie kształtu zmienia również zaznaczenie i kolejność, zapamiętanie samego identyfikatora usuniętego kształtu nie wystarczy. Migawkę często łatwiej skontrolować, gdy operacja wpływa na kilka powiązanych wartości.
Kiedy migawka jest rzeczywiście niezależna?
Struktury i tablice w Swift mają semantykę wartości. Tablica kształtów zbudowana wyłącznie z typów wartości może więc reprezentować wcześniejszy stan niezależnie od późniejszych modyfikacji. Kopiowanie tablicy instancji klas zachowuje jednak odwołania do tych instancji; zmiana instancji może zmienić również to, co obserwuje domniemana migawka. Opakowanie tych odwołań w strukturę nie usuwa tego problemu. Typy wartości i typy referencyjne w Swift
W małym edytorze używaj do historii zwykłych danych wartościowych: identyfikatorów, współrzędnych, wymiarów i innych atrybutów edytowalnych. Widoki i mutowalne obiekty renderujące trzymaj poza migawką. Jeśli model na żywo korzysta z klas, utwórz osobną reprezentację wartościową zawierającą wszystkie atrybuty potrzebne do jego przywrócenia.
Nie zakładaj stałego kosztu pamięci na podstawie samego słowa „migawka”. Zmierz pamięć zajmowaną przez zachowaną historię i opóźnienia edycji na reprezentatywnych syntetycznych płótnach. Przykładowe wyliczenie rozmiaru danych: 100 wpisów, z których każdy zachowuje 1 000 rekordów kształtów, oznacza 100 000 wystąpień rekordów; 100 wpisów dotyczących odwrotnych operacji na pojedynczym kształcie zachowuje tylko 100 poprzednich środków wraz z identyfikatorami. Te liczby nie określają zmierzonego rozmiaru w bajtach ani nie są twierdzeniem o alokacji pamięci przez Swift.
Ustal świadomie głębokość historii. Apple dokumentuje, że levelsOfUndo ogranicza liczbę grup cofania najwyższego poziomu, a domyślna wartość zero oznacza brak limitu. Limit grup jest przydatny, ale nie nakłada budżetu bajtów: migawka partii i edycja jednej współrzędnej mogą zachowywać bardzo różne ilości danych. levelsOfUndo
Jak migawki i polecenia odwrotne zachowują możliwość ponawiania?
Metoda przywracania powinna zarejestrować odwrotne działanie przywracające przed zmianą modelu. Gdy cofnięcie wywoła tę metodę, jej rejestracja umożliwi ponowienie. W przypadku migawki przed przywróceniem poprzedniego stanu zapisz stan aktualnie zainstalowany. W przypadku polecenia pozycji przed ustawieniem zapamiętanego środka zapisz środek bieżący.
Zarchiwizowany przewodnik Apple po architekturze cofania jasno opisuje ten wzorzec wzajemnej rejestracji: ustawienie wywołane podczas cofania rejestruje wartość potrzebną do ponowienia. Przykłady używają Objective-C; pełny przykład w Swift poniżej korzysta z aktualnego interfejsu API opartego na domknięciach. Rejestrowanie operacji cofania
Nie pomijaj rejestracji tylko dlatego, że isUndoing ma wartość true. W tym projekcie usunęłoby to rejestrację tworzącą możliwość ponowienia. Unikaj też odtwarzania nowego ruchu względnego podczas przywracania pozycji: zapisanie dokładnego poprzedniego środka jasno określa zamierzone przywrócenie.
Pełny przykład na poziomie modelu
Ten samodzielny program dla macOS łączy usuwanie oparte na migawce z ustawieniem pozycji za pomocą polecenia odwrotnego. Korzysta wyłącznie z syntetycznych wartości przechowywanych w pamięci i z Foundation, bez zależności od trwałego przechowywania danych ani interfejsu użytkownika. Zapisz go jako EditorUndo.swift i skompiluj przy użyciu aktualnego toolchaina Swift 6 oraz SDK macOS:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Program celowo korzysta z dedykowanego menedżera i wyłącza grupowanie zdarzeń, aby jego asercje nie zależały od działającej pętli zdarzeń aplikacji. Każda początkowa edycja otrzymuje jawną grupę. Metody przywracania nie otwierają dodatkowych grup podczas cofania ani ponawiania. Apple wymaga jawnego zamknięcia grup przed cofnięciem, gdy automatyczne grupowanie jest wyłączone. 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")
}
}
Oczekiwany wynik to Undo and redo checks passed. Przykład został sprawdzony, ale nie został tutaj uruchomiony, ponieważ w środowisku badawczym nie było dostępnego kompilatora Swift. Pokazuje podstawową obsługę historii; interfejs SwiftUI wymaga również obserwowalnego modelu lub adaptera, aby odświeżać widoki.
Domknięcie korzysta z parametru target, zamiast bezpośrednio przechwytywać edytor. Apple dokumentuje, że cel rejestracji musi być typem referencyjnym, menedżer przechowuje do niego niezachowywane odwołanie, a użycie parametru domknięcia pozwala uniknąć cyklu referencji poprzez przechwycony cel. registerUndo(withTarget:handler:)
Warunki wstępne ujawniają błędne założenia modelu, zamiast po cichu pomijać operację cofania. Granica polecenia w produkcyjnym rozwiązaniu powinna odrzucać nieprawidłową edycję przed rejestracją i zgłaszać odpowiedni błąd na poziomie aplikacji. Brakujące identyfikatory podczas odtwarzania historii wskazują, że coś zmieniło model poza oczekiwaną sekwencją historii.
Jak przeciąganie powinno stać się jedną czynnością możliwą do cofnięcia?
Przeciąganie generuje wiele aktualizacji, a użytkownik oczekuje, że jedno zakończone przesunięcie będzie można cofnąć jako całość. Automatyczne grupowanie Apple podąża za cyklami pętli zdarzeń, więc nie można traktować go jako definicji całego gestu. Jawne grupy mogą łączyć rejestracje, ale ich granice nadal wymagają świadomego zarządzania. groupsByEvent, beginUndoGrouping()
W tym edytorze zastosuj sekwencję podglądu i zatwierdzenia:
- Po rozpoczęciu przeciągania zachowaj identyfikator celu i zatwierdzony środek początkowy jako punkt odniesienia dla gestu.
- Użyj
@GestureStateiupdating(_:body:)do tymczasowego przesunięcia. Renderuj kształt w pozycji równej punktowi odniesienia plus to przesunięcie, nie zmieniając zatwierdzonych elementów. - Po pomyślnym zakończeniu przez
onEnded(_:)oblicz końcowy środek na podstawie końcowej wartości gestu i jednokrotnie wywołaj metodę modelu ustawiającą pozycję z obsługą cofania. - Pomiń rejestrację, jeśli wynikowy środek jest równy zatwierdzonemu środkowi.
- W przypadku anulowania odrzuć podgląd i zachowaj zatwierdzony model.
Ta sekwencja odpowiada udokumentowanemu przez SwiftUI rozróżnieniu: stan gestu resetuje się po zakończeniu lub anulowaniu gestu, a onEnded(_:) uruchamia się tylko po pomyślnym zakończeniu. W związku z tym sprzątanie, które musi nastąpić również po anulowaniu, nie może zależeć wyłącznie od onEnded(_:). Dodawanie interaktywności za pomocą gestów
Podczas podglądu utrzymuj stabilny stan zatwierdzony edytora. Jedna prosta zasada to wyłączenie innych modyfikacji płótna i cofania na płótnie, gdy przeciąganie jest aktywne, a następnie ponowne ich włączenie po zakończeniu lub anulowaniu. W przeciwnym razie określ, w jaki sposób edycja wykonana w międzyczasie unieważnia zapisany punkt odniesienia przed zatwierdzeniem przeciągnięcia.
Jeśli inna część edytora musi stale obserwować aktualizowane współrzędne zatwierdzone, renderowanie wyłącznie podglądu może się nie sprawdzić. W takim przypadku zaimplementuj jawną transakcję gestu: zapisz stan początkowy, stosuj aktualizacje pośrednie bez osobnych wpisów historii i zarejestruj jedno przywrócenie po pomyślnym zakończeniu. Anulowanie musi przywrócić stan początkowy. Nie pozostawiaj otwartej współdzielonej grupy cofania między dowolnymi wywołaniami zwrotnymi ani operacjami asynchronicznymi.
Kto powinien być właścicielem modelu i menedżera cofania?
Dla każdego niezależnego edytowalnego płótna zachowaj jednego stabilnego właściciela edytora. Jest on właścicielem zatwierdzonych elementów i celem operacji historii. Zarówno modyfikacje modelu, jak i interakcje z menedżerem cofania wykonuj na aktorze głównym; aktualna dokumentacja Apple oznacza UndoManager i jego obsługę domknięcia jako izolowane na aktorze głównym. UndoManager
W SwiftUI pobierz menedżer widoku za pomocą @Environment(\.undoManager). Przekaż go przez stabilną warstwę edytora lub poleceń, zamiast tworzyć nowy menedżer przy każdym obliczeniu widoku. Wartość środowiskowa jest opcjonalna: Apple dokumentuje, że w nieobsługiwanych kontekstach zwraca nil i zezwala na pominięcie rejestracji cofania. Edycja nadal może być możliwa, ale taka ścieżka nie powinna informować, że cofanie jest dostępne. EnvironmentValues.undoManager
W przypadku płótna AppKit wyszukaj menedżera przez odpowiedniego respondera. Podstawowa implementacja NSResponder.undoManager pyta następnego respondera. Dlatego prywatnie utworzony menedżer wymaga jawnej integracji z kontekstem responderów płótna, jeśli standardowe polecenia menu mają do niego docierać. Samo przechowywanie go w modelu nie tworzy takiego połączenia. NSResponder.undoManager
Konfiguracja grupowania w samodzielnym przykładzie dotyczy jego dedykowanego menedżera. Nie kopiuj tej konfiguracji do menedżera przekazanego przez środowisko, jeśli nie zarządzasz zasadami grupowania wszystkich uczestników. W interfejsie zarejestruj zakończoną edycję płótna, korzystając z istniejących zasad obsługi zdarzeń danego kontekstu; grupuj kilka synchronicznych podoperacji, gdy składają się na jedną czynność.
Zanim usuniesz cel edytora, usuń jego oczekujące rejestracje w kontrolowanym punkcie cyklu życia. Apple udostępnia removeAllActions(withTarget:) do usuwania operacji cofania i ponawiania przypisanych do tego celu. Wyczyszczenie całego menedżera wpłynęłoby również na innych uczestników, jeśli menedżer jest współdzielony. removeAllActions(withTarget:)
Co należy sprawdzić przed wyborem ostatecznego projektu?
Przećwicz krótką sekwencję przekraczającą granice reprezentacji: przesuń kształt, usuń inny, cofnij obie czynności, a następnie ponów obie. Po każdym kroku sprawdzaj wartości, identyfikatory i kolejność. Dodaj anulowane przeciągnięcie, przeciągnięcie zakończone w pozycji początkowej i dwa niezależne okna edytora. Każdy przypadek sprawdza inną granicę: dane przywracania, zakończenie gestu, obsługę operacji pustej lub własność.
Traktuj masową podmianę płótna jako jawną operację. Zdecyduj, czy ma być możliwa do cofnięcia z wystarczającym stanem przywracania, czy po niej ma powstać nowy punkt odniesienia przez celowe wyczyszczenie nieaktualnej historii. Migawka pomijająca edytowalne pola i polecenie odwrotne, które nie potrafi znaleźć celu, to niekompletne implementacje.
Zacznij od migawek dla małego płótna opartego na wartościach, zachowaj jednego stabilnego właściciela i zatwierdzaj każdy gest jednokrotnie. Wprowadź polecenia odwrotne tam, gdzie dane przywracania są jasne, a pomiary uzasadniają dodatkową logikę. Dzięki temu można zmienić reprezentację historii bez zmiany znaczenia pojedynczej czynności użytkownika.
Źródła
- UndoManager — Apple
- registerUndo(withTarget:handler:) — Apple
- groupsByEvent — Apple
- beginUndoGrouping() — Apple
- levelsOfUndo — Apple
- EnvironmentValues.undoManager — Apple
- NSResponder.undoManager — Apple
- Dodawanie interaktywności za pomocą gestów — Apple
- removeAllActions(withTarget:) — Apple
- Rejestrowanie operacji cofania — Apple, archiwum dokumentacji
- Typy wartości i typy referencyjne w Swift — Swift.org