Bellek içi çalışan küçük bir Mac şekil düzenleyicisi için değer durumu anlık görüntüleriyle başlayın. Daha küçük yüklerinin sağladığı yarar ek mantığa değiyorsa, sık yapılan ve kapsamı dar düzenlemelerde ters komutları kullanın. Her iki yaklaşım da tek bir kararlı sahip ve her kullanıcı eylemi için açık bir tamamlama sınırı gerektirir.
Bu kılavuz tek bir görevi ele alıyor: kendi Swift düzenleyicinizde sentetik şekilleri taşıma, silme ve yeniden sıralama işlemleri için öngörülebilir geri alma ve yineleme uygulamak. Aşağıdaki karar çerçevesi, Apple API’lerinden türetilmiş bir mühendislik önerisidir; Apple, anlık görüntü ile komut arasında seçim yapmak için evrensel bir kural belirtmez.
Her geri alma işlemi neyi hatırlamalı?
Anlık görüntü, sıralı bir şekil dizisi gibi önceki düzenlenebilir durumu kaydeder. Geri alma bu diziyi geri yükler. Ters komut ise belirli bir düzenlemeyi geri çevirmek için gereken işlemi, örneğin bir şeklin merkezini önceki koordinatlarına döndürmeyi kaydeder.
Her iki gösterim de Foundation’ın UndoManager yapısı üzerinden çalışır. Bu yapı işlemleri bir nesneye veya onun sahibine kaydeder; modelinizdeki rastgele atamaları kendiliğinden gözlemlemez. Uygun geri yükleme işlemini kaydetmek için mutasyon yöntemlerinizin bunu yapması gerekir. UndoManager
Geçmişin nasıl temsil edileceğine karar vermeden önce düzenlenebilir durumu tanımlayın. Bu düzenleyicide buna şekil kimlikleri, koordinatlar ve dizi sırası dahildir. Üzerine gelme göstergelerini ve sürükleme önizlemelerini bunun dışında tutun. Seçimin geçmişe dahil olup olmayacağına açıkça karar verin: seçimi geri yüklemek silme işlemini anlamayı kolaylaştırabilir, ancak geri almanın mevcut seçimi de değiştirmesi anlamına gelir.
Tüm düzenleyicinin tek bir gösterim kullanmasını şart koşmak yerine, her işlem için şu karar tablosunu kullanın:
| İşlem veya koşul | Önerilen gösterim | Saklanacak bilgiler |
|---|---|---|
| Küçük tuval; birlikte değişen birkaç özellik | Anlık görüntü | Önceki düzenlenebilir değerin tamamı |
| Bir şekli taşıma | Ters ayarlayıcı | Kararlı şekil kimliği ve önceki merkez |
| Bir şekli silme | Başlangıçta anlık görüntü; gerekirse ters komut | Ters işlem için: silinen şeklin tamamı ve özgün konumu |
| Birden çok şekli yeniden sıralama | Ters işlem basitse bunun dışındaki durumlarda anlık görüntü | Önceki sıralama ve etkilenen özellikler |
| Ölçülen geçmiş belleği maliyeti aşırı | Hedefli ters komutlar | Tam geri yükleme için gereken her şey |
Asıl soru şudur: ters işlemin geri yüklemesi gereken her şeyi sayabiliyor musunuz? Bir şeklin silinmesi seçimi ve sıralamayı da değiştiriyorsa, yalnızca silinen kimliği hatırlamak yetersizdir. İşlem birbiriyle ilişkili birkaç değeri etkilediğinde anlık görüntüyü denetlemek çoğu zaman daha kolaydır.
Anlık görüntü gerçekte ne zaman bağımsızdır?
Swift yapıları ve dizileri değer semantiğine sahiptir. Bu nedenle yalnızca değer türlerinden oluşturulmuş bir şekil dizisi, sonraki mutasyonlardan bağımsız önceki bir durumu temsil edebilir. Ancak sınıf örneklerinden oluşan bir diziyi kopyalamak, bu örneklere yönelik başvuruları korur; bir örneğin değiştirilmesi, sözde anlık görüntünüzün gördüğü değeri de değiştirebilir. Bu başvuruları bir yapıyla sarmak sorunu ortadan kaldırmaz. Value And Reference Types In Swift
Küçük bir düzenleyicide geçmiş için yalın değer verileri kullanın: tanımlayıcılar, koordinatlar, boyutlar ve düzenlenebilir diğer öznitelikler. Görünümleri ve değiştirilebilir çizim nesnelerini anlık görüntünün dışında tutun. Canlı model sınıflar kullanıyorsa, modeli geri yüklemek için gereken tüm öznitelikleri içeren ayrı bir değer gösterimi oluşturun.
“Anlık görüntü” sözcüğünden sabit bir bellek maliyeti çıkarmayın. Geçmişte tutulan verileri ve düzenleme gecikmesini temsili sentetik tuvallerle ölçün. Örnek bir yük hesabı olarak, her biri 1.000 şekil kaydı tutan 100 girdi, toplam 100.000 kayıt oluşumu demektir; tek şekilli 100 ters girdi ise yalnızca 100 önceki merkezi ve tanımlayıcıları tutar. Bu sayılar ölçülmüş bayt miktarları değildir ve Swift’in bellek ayırması hakkında bir iddia oluşturmaz.
Geçmiş derinliğini bilinçli olarak belirleyin. Apple, levelsOfUndo değerinin üst düzey geri alma gruplarını sınırladığını ve varsayılan değerinin 0, yani sınırsız grup olduğunu belgeliyor. Grup sınırı yararlıdır, ancak bayt bütçesi getirmez: toplu anlık görüntü ile tek koordinatlık düzenleme çok farklı miktarlarda veri tutabilir. levelsOfUndo
Anlık görüntüler ve ters komutlar yinelemeyi nasıl korur?
Geri yükleme yöntemi, modeli değiştirmeden önce karşıt geri yüklemeyi kaydetmelidir. Geri alma bu yöntemi çalıştırdığında, kaydedilen işlem yinelemeyi sağlar. Anlık görüntü için önce o anda yüklü olan durumu yakalayın, ardından önceki durumu geri yükleyin. Konum komutu için de kayıtlı merkezi yüklemeden önce mevcut merkezi yakalayın.
Apple’ın arşivlenmiş geri alma mimarisi kılavuzu bu karşılıklı kayıt düzenini açıkça anlatır: geri alma sırasında çağrılan bir ayarlayıcı, yineleme için gereken değeri kaydeder. Örneklerinde Objective-C kullanılmıştır; aşağıdaki Swift örneği güncel closure API’sini kullanır. Registering Undo Operations
isUndoing true olduğu için kayıt yapmayı engellemeyin. Bu tasarımda böyle yapmak yinelemeyi oluşturan kaydı kaldırır. Ayrıca bir konumu geri yüklerken yeni bir göreli hareket uygulamaktan kaçının: önceki tam merkezi saklamak, amaçlanan geri yüklemeyi açıkça belirtir.
Model düzeyinde eksiksiz bir örnek
Bu bağımsız macOS programı, anlık görüntü tabanlı silme işlemini ters konum ayarlayıcısıyla birleştirir. Kalıcılık veya kullanıcı arayüzü bağımlılıkları olmadan yalnızca sentetik bellek içi değerler ve Foundation kullanır. Programı EditorUndo.swift adıyla kaydedip güncel bir Swift 6 araç zinciri ve macOS SDK ile derleyin:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Program, çalışan bir uygulama olay döngüsüne bağlı kalmaması için özel bir yöneticiyi sahiplenir ve olay gruplandırmasını devre dışı bırakır. Her ilk düzenlemeye açıkça bir grup atanır. Geri yükleme yöntemleri, geri alma veya yineleme sırasında ek grup açmaz. Apple, otomatik gruplandırma devre dışı bırakıldığında geri almadan önce grupların açıkça kapatılmasını şart koşar. 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")
}
}
Beklenen çıktı Undo and redo checks passed olur. Araştırma ortamında Swift derleyicisi bulunmadığından örnek burada incelendi, ancak çalıştırılmadı. Örnek geçmişin temelini gösterir; SwiftUI arayüzünde görünümleri yenilemek için ayrıca gözlemlenebilir bir model veya bağdaştırıcı gerekir.
Closure, düzenleyiciyi doğrudan yakalamak yerine target parametresini kullanır. Apple, kayıt hedefinin bir başvuru türü olması gerektiğini, yöneticinin ona sahipsiz bir başvuru tuttuğunu ve closure parametresini kullanmanın yakalanan hedef üzerinden oluşabilecek bir güçlü başvuru döngüsünü önlediğini belgeliyor. registerUndo(withTarget:handler:)
Ön koşullar, model varsayımlarındaki bozuklukları geri alma işlemini sessizce atlamak yerine görünür kılar. Üretim ortamındaki bir komut sınırı, geçersiz bir düzenlemeyi kayıt yapılmadan önce reddetmeli ve uygun bir uygulama düzeyi hatası göstermelidir. Geçmiş yeniden oynatılırken kimliklerin bulunamaması, modelin beklenen geçmiş sırasının dışında değiştirildiğine işaret eder.
Sürükleme nasıl tek bir geri alınabilir eyleme dönüşmeli?
Sürükleme çok sayıda güncelleme üretir, ancak kullanıcı tamamlanmış tek bir hareketi geri almayı bekler. Apple’ın otomatik gruplandırması olay döngüsü geçişlerini izler; bu nedenle tüm hareketin tanımı olarak kullanılamaz. Açık gruplar kayıtları birleştirebilir, ancak sınırlarının yine bilinçli biçimde yönetilmesi gerekir. groupsByEvent, beginUndoGrouping()
Bu düzenleyici için önizleme ve tamamlama sırasını kullanın:
- Sürükleme başladığında hedef kimliği ve kaydedilmiş başlangıç merkezini hareketin referans noktası olarak saklayın.
- Geçici öteleme için
@GestureStateveupdating(_:body:)kullanın. Kaydedilmiş öğeleri değiştirmeden şekli, başlangıç noktası artı bu öteleme konumunda çizin. onEnded(_:)başarıyla tamamlandığında, son hareket değerinden son merkezi hesaplayın ve modelin geri alınabilir ayarlayıcısını bir kez çağırın.- Sonuç merkez kaydedilmiş merkezle aynıysa kayıt oluşturmayın.
- İptal durumunda önizlemeyi atın ve kaydedilmiş modeli koruyun.
Bu sıralama, SwiftUI belgelerinde açıklanan ayrıma uyar: hareket sona erdiğinde veya iptal edildiğinde hareket durumu sıfırlanır; onEnded(_:) ise yalnızca başarıyla tamamlandığında çalışır. Bu nedenle, iptalde de çalışması gereken temizleme işlemi yalnızca onEnded(_:) öğesine bağlı olmamalıdır. Adding interactivity with gestures
Önizleme sırasında düzenleyicinin kaydedilmiş durumunu sabit tutun. Basit bir politika, sürükleme etkin olduğunda tuvaldeki diğer mutasyonları ve tuvalin geri alma işlemini devre dışı bırakmak, ardından tamamlama veya iptal sonrasında yeniden etkinleştirmektir. Aksi durumda, sürükleme tamamlamasını kabul etmeden önce araya giren bir düzenlemenin kaydedilmiş referans noktasını nasıl geçersiz kıldığını tanımlayın.
Düzenleyicinin başka bir bölümü sürekli güncellenen kaydedilmiş koordinatları gözlemlemek zorundaysa, yalnızca önizlemede çizim yapmak uygun olmayabilir. Böyle bir durumda açık bir hareket işlemi uygulayın: başlangıç durumunu kaydedin, ara güncellemeleri ayrı geçmiş girdileri oluşturmadan uygulayın ve başarılı tamamlanmada tek bir geri yükleme kaydedin. İptal durumunda başlangıç durumunu geri yükleyin. Herhangi bir geri çağırım veya eşzamansız iş boyunca paylaşılan bir geri alma grubunu açık bırakmayın.
Modelin ve geri alma yöneticisinin sahibi kim olmalı?
Bağımsız olarak düzenlenebilen her tuval için tek bir kararlı düzenleyici sahibi bulundurun. Bu sahip, kaydedilmiş öğeleri tutar ve geçmiş işlemlerinin hedefidir. Model mutasyonlarını ve geri alma etkileşimlerini ana aktörde tutun; Apple’ın güncel belgeleri UndoManager ve closure işleyicisini ana aktörle yalıtılmış olarak işaretler. UndoManager
SwiftUI’da görünüm yöneticisini @Environment(\.undoManager) ile alın. Her görünüm değerlendirmesinde yeni bir yönetici oluşturmak yerine, bu yöneticiyi kararlı bir düzenleyici veya komut katmanına aktarın. Ortam değeri isteğe bağlıdır: Apple, desteklenmeyen bağlamlarda nil döndüğünü belgeliyor ve bu durumda geri alma kaydının atlanmasına izin veriyor. Düzenleme yine de yapılabilir, ancak bu yol kullanılabilir bir geri alma olanağı sunuyormuş gibi gösterilmemelidir. EnvironmentValues.undoManager
AppKit tuvali için yöneticiyi uygun yanıtlayıcı üzerinden bulun. NSResponder.undoManager temel uygulaması bir sonraki yanıtlayıcıya sorar. Bu nedenle, standart menü eylemlerinin özel olarak oluşturduğunuz bir yöneticiye ulaşması bekleniyorsa, yöneticiyi tuvalin yanıtlayıcı bağlamına açıkça bağlamanız gerekir. Yöneticiyi modelde saklamak tek başına bu bağlantıyı kurmaz. NSResponder.undoManager
Bağımsız örnekteki gruplandırma yapılandırması, özel yöneticisine aittir. Tüm katılımcıların gruplandırma politikasına sahip değilseniz, bu yapılandırmayı ortamdan gelen bir yöneticiye kopyalamayın. Kullanıcı arayüzünde, tamamlanan tuval düzenlemesini bağlamın mevcut olay politikasını kullanarak kaydedin; tek bir eylem oluşturan eşzamanlı alt işlemleri gruplayın.
Bir düzenleyici hedefini elden çıkarmadan önce, bekleyen kayıtlarını denetimli bir yaşam döngüsü sınırında kaldırın. Apple, o hedefe yönelik geri alma ve yineleme işlemlerini kaldırmak için removeAllActions(withTarget:) sağlar. Paylaşılan bir yöneticinin tümünü temizlemek diğer katılımcıları da etkiler. removeAllActions(withTarget:)
Son tasarımı seçmeden önce neleri doğrulamalısınız?
Gösterim sınırlarını aşan kısa bir işlem dizisini deneyin: bir şekli taşıyın, diğerini silin, ikisini de geri alın, sonra ikisini de yineleyin. Her adımdan sonra değerleri, kimlikleri ve sıralamayı kontrol edin. İptal edilen bir sürükleme, başlangıç konumunda biten bir sürükleme ve birbirinden bağımsız iki düzenleyici penceresi ekleyin. Her durum farklı bir sınırı sınar: geri yükleme verisi, hareketin tamamlanması, işlem yapılmaması veya sahiplik.
Tuvalin toplu olarak değiştirilmesini açık bir işlem olarak ele alın. Ya yeterli geri yükleme durumuyla geri alınabilir hâle getirin ya da eski geçmişi bilerek temizledikten sonra yeni bir başlangıç noktası belirleyin. Düzenlenebilir alanları atlayan bir anlık görüntü de hedefini bulamayan bir ters komut da eksik bir uygulamadır.
Değer temelli küçük bir tuval için anlık görüntülerle başlayın, tek bir kararlı sahip bulundurun ve her hareketi bir kez tamamlayın. Geri yükleme verisinin açık olduğu ve ölçümlerin ek mantığı haklı çıkardığı yerlerde ters komutları devreye alın. Böylece tek bir kullanıcı eyleminin anlamını değiştirmeden geçmiş gösterimini değiştirebilirsiniz.
Kaynaklar
- 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