Undo işlemlerini uzun ömürlü bir model store’a kaydedin ve görünümlerin düzenlemeleri store yöntemleri üzerinden göndermesini sağlayın. Mac için bir şekil düzenleyicide sürükleme önizlemelerini yerel tutun, bırakıldığında bir kez kaydedin ve redo işlemini de mümkün kılan inverse’i store’a kaydettirin.
Bu öğreticide tek bir sentetik dikdörtgen içeren küçük bir macOS SwiftUI uygulaması oluşturuluyor. Dikdörtgeni taşıyabilir, düzenleyicisini gizleyebilir, taşımayı undo edebilir ve düzenleyiciyi yeniden gösterebilirsiniz. Somut görev, düzenleme görünümü kaybolurken modeli yaşamaya devam ettiğinde undo davranışını korumaktır. Her şey bellekte kalır.
Undo işlemini neden store almalı?
Apple’ın closure tabanlı kayıt yöntemi, hedefin bir başvuru türünde olmasını gerektirir. Ayrıca undo yöneticisinin bu hedefe sahipsiz bir başvuru tuttuğunu belirtir. Dolayısıyla bir işlemi kaydetmek, hedefin yaşam süresini belirlemez. Apple, aynı hedefe başka bir başvuru yakalamak yerine işleyicinin hedef parametresinin kullanılmasını önerir. Apple’ın undo kaydı belgeleri.
Düzenlenen duruma sahip bir store, uygun bir hedeftir. Kararlı bir sahip bu store’u yaşatır; geçici görünümler ise onu gösterir ve değişiklik talep eder. Burada pencerenin kök görünümü store’a @StateObject ile sahip olurken, kaldırılabilir tuval onu gözlemler. SwiftUI, bir durum nesnesini onu tanımlayan kapsayıcının yaşam süresi boyunca korur; ancak kapsayıcının kimliğini değiştirmek yeni bir örnek oluşturabilir. Apple’ın StateObject belgeleri.
“Store’un sahip olduğu inverse’ler”, geri alma mantığının nerede bulunduğunu anlatır. Geri çağrıları hâlâ undo yöneticisi kaydeder ve zamanlar. Store, eski değeri ve onu geri yükleyen yöntemi sağlar. Bu ayrım, undo alıcısının değiştirilen nesnenin kendisi veya onun sahibi olabileceğini belirten Apple belgelerindeki mimariyi izler. Apple’ın Undo Manager kılavuzu.
| Sorumluluk | Bu örnekteki sahibi |
|---|---|
| Kaydedilmiş dikdörtgen konumu | Model store |
| Inverse kaydı ve değişiklik | Model store yöntemi |
| Store’un yaşam süresi | Kararlı pencere kökü |
| Geçici sürükleme ötelemesi | Tuval hareket durumu |
| Undo geçmişi ve yürütme | Ortam undo yöneticisi |
| Uygun yöneticiyi seçme | Etkileşim sınırındaki görünüm |
Geri alınabilir bir store değişikliği uygulayın
macOS 13 veya sonraki sürümleri hedefleyen ve güncel bir Apple SDK kullanan bir macOS SwiftUI uygulaması oluşturun. Aşağıdaki üç dosya örneğin tamamını oluşturur; ikinci bir @main bildirimi bırakmak yerine oluşturulan uygulama giriş noktasını ve içerik görünümünü değiştirin. Bu araştırma ortamında Swift derleyicisi ve macOS çerçeveleri bulunmadığından örnek incelendi, ancak burada derlenmedi.
ShapeStore.swift dosyasını oluşturun. Store’da kaydedilmiş tek bir konum ve tek bir değişiklik yöntemi bulunur. Atamadan önce önceki konumu yakalamak, yönteme düzenlemeyi geri almak için gereken bilgiyi verir.
import Foundation
import Combine
@MainActor
final class ShapeStore: ObservableObject {
@Published private(set) var position = CGPoint(x: 120, y: 100)
func move(to newPosition: CGPoint, undoManager: UndoManager?) {
let previousPosition = position
guard newPosition != previousPosition else { return }
if let undoManager {
undoManager.registerUndo(withTarget: self) {
[weak undoManager] store in
store.move(
to: previousPosition,
undoManager: undoManager
)
}
undoManager.setActionName("Move Rectangle")
}
position = newPosition
}
}
Inverse aynı yöntemi çağırır. Konum (120, 100) değerinden (160, 100) değerine değişsin. Kaydedilen geri çağrı (120, 100) değerini ister. Undo bu geri çağrıyı yürüttüğünde yöntem, önceki değeri (160, 100) olarak görür ve geri dönüş işlemini kaydeder. Apple, undo sırasında yapılan kayıtların redo işlemlerine dönüştüğünü belirtir. isUndoing true olduğunda kayıt yapmamak bu düzeni bozar. Apple’ın Undo Manager kılavuzu.
Closure, store parametresi üzerinde çalışır ve yöneticiyi zayıf başvuru ile yakalar. Bu, yöneticinin sakladığı closure’dan tekrar o yöneticiye güçlü bir başvuru oluşmasını önler. Swift’in başvuru sayımı belgeleri, zayıf başvuruların işaret ettikleri örneği yaşatmadığını ve örnek serbest bırakıldığında nil hâline geldiğini açıklar. Swift Programlama Dili: Otomatik Başvuru Sayımı.
Store ve değişiklik yöntemi ana aktör üzerinde yalıtılmıştır. Bu, güncel Apple belgeleriyle uyumludur: UndoManager ve kayıt işleyicisi ana aktör üzerinde yalıtılmıştır. Yöntem, kaydı ve değişikliği eşzamanlı olarak gerçekleştirerek bu küçük düzenlemeyi onu çağıran etkileşim içinde tutar. Apple’ın UndoManager belgeleri.
Store’u kaldırılabilir düzenleyicinin üzerinde tutun
StoreUndoDemoApp.swift dosyasını oluşturun. Tuval görünür olsun ya da olmasın, kök görünüm store’a sahip olur. Düğmeleri, bir işlem çalıştırıldığında ortam yöneticisinin o anki değerini okur. Tuval, düzenlemeleri için aynı yöneticiyi alır.
import SwiftUI
@main
struct StoreUndoDemoApp: App {
var body: some Scene {
WindowGroup {
EditorRoot()
}
}
}
@MainActor
struct EditorRoot: View {
@StateObject private var store = ShapeStore()
@State private var showsEditor = true
@Environment(\.undoManager) private var undoManager
var body: some View {
VStack(spacing: 16) {
HStack {
Button("Move Right") {
store.move(
to: CGPoint(
x: store.position.x + 20,
y: store.position.y
),
undoManager: undoManager
)
}
Button("Undo") {
if let undoManager, undoManager.canUndo {
undoManager.undo()
}
}
Button("Redo") {
if let undoManager, undoManager.canRedo {
undoManager.redo()
}
}
Toggle("Show Editor", isOn: $showsEditor)
}
Text(
"Position: \(Int(store.position.x)), " +
"\(Int(store.position.y))"
)
if showsEditor {
ShapeCanvas(store: store, undoManager: undoManager)
} else {
Text("Editor hidden")
.frame(width: 400, height: 260)
}
}
.padding(20)
.frame(width: 620, height: 380)
}
}
SwiftUI’ın ortam undo yöneticisi isteğe bağlıdır. Apple, ortam nil sağladığında undo kaydının atlanmasına açıkça izin verir. Bu örnek o durumda da dikdörtgeni taşır, ancak undo girdisi oluşturmaz. Beklenen undo işlemi yoksa inverse mantığını değiştirmeden önce etkileşime bir yöneticinin ulaşıp ulaşmadığını inceleyin. Apple’ın ortam undoManager belgeleri.
Yöneticiyi her değişikliğe geçirmek, pencere bağlamını da açık tutar. Store, karşısına ilk çıkan pencere yöneticisini saklamaz. Bu örnekte her pencere kökü kendi store’una sahiptir. Birden fazla pencere daha sonra aynı paylaşılan modeli düzenlerse, bu düzeni pencereler arasında yeniden kullanmadan önce bu düzenlemelerin geçmişinin hangi pencereye ait olacağını belirleyin.
Undo grubunu olaylar boyunca açık tutmadan tek bir sürüklemeyi kaydedin
ShapeCanvas.swift dosyasını oluşturun. Sürükleme sırasında dikdörtgenin görüntülenen konumuna geçici bir öteleme eklenir. Store’daki kaydedilmiş konum yalnızca hareket başarıyla tamamlandığında değişir. Başka bir düzenleme veya undo komutu çalıştırmadan önce sürüklemeyi tamamlayın; bu kısa örnek sıralı etkileşimleri varsayar ve sürükleme sırasında eşzamanlı komutları yönetmez.
import SwiftUI
@MainActor
struct ShapeCanvas: View {
@ObservedObject var store: ShapeStore
let undoManager: UndoManager?
@GestureState private var translation: CGSize = .zero
var body: some View {
ZStack(alignment: .topLeading) {
Color.gray.opacity(0.12)
Rectangle()
.fill(Color.blue)
.frame(width: 70, height: 50)
.position(
x: store.position.x + translation.width,
y: store.position.y + translation.height
)
.gesture(
DragGesture()
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
store.move(
to: CGPoint(
x: store.position.x
+ value.translation.width,
y: store.position.y
+ value.translation.height
),
undoManager: undoManager
)
}
)
}
.frame(width: 400, height: 260)
.clipped()
}
}
Hareketi kaydetmek için gesture’ın son ötelemesi kullanılır. Önizleme özelliğinin ne zaman sıfırlanacağına bağlı değildir. Apple, updating(_:body:) özelliğinin gesture sona erdiğinde veya iptal edildiğinde gesture durumunu sıfırladığını, onEnded(_:) özelliğinin ise yalnızca başarıyla tamamlandığında çalıştığını belirtir. Bu nedenle, bu tasarımda iptal edilen bir gesture kaydedilmiş bir hareket veya yeni bir undo işlemi bırakmaz. Apple’ın gesture güncelleme belgeleri, Apple’ın gesture tamamlama belgeleri.
Bu yaklaşım üç ayrı sınırı korur: önizleme güncellemesi geçici görüntüleme durumunu değiştirir; başarılı tamamlama bir model düzenlemesi ister; store bu düzenlemenin inverse’ini kaydeder. Store değişikliğini her sürükleme güncellemesinde çağırmak, gesture boyunca ara konumları kaydederdi.
Apple’ın varsayılan olay gruplaması, run loop geçişleri çevresindeki kayıtları bir araya getirir. Sürükleme birden fazla geçişte güncellemeler üretebilir; dolayısıyla yalnızca olay gruplaması, sürüklemenin tamamını kapsayan istenen işlemi ifade etmez. Burada bir kez kaydetmek, paylaşılan yöneticinin yapılandırmasını değiştirmeden bu sorunu önler. Apple’ın groupsByEvent belgeleri.
Birden fazla öğeyi eşzamanlı değiştiren gelecekteki bir komut için açık ve dengeli bir undo grubu, inverse’leri bir araya getirebilir. Başlangıcını ve sonunu bu komutun içinde tutun. Sürükleme sırasında modelin canlı olarak değiştirilmesi gerekirse, diğer komutların etkin düzenlemeyle nasıl etkileşeceğini de tanımlayan ayrı başlatma, güncelleme, kaydetme ve iptal işlemleri tasarlayın. Buradaki yalnızca önizleme yaklaşımı, bu ek işlem altyapısını bilerek store’un dışında tutar.
Sahipliği, redo’yu ve pencere yönlendirmesini ayrı ayrı doğrulayın
Çalışan uygulamayla aşağıdaki sırayı izleyin. Koordinatlar, ölçülmüş sonuçlar değil, verilen başlangıç durumu ve düğme artışından beklenen örnek değerlerdir.
| İşlem | Beklenen kaydedilmiş konum veya sonuç |
|---|---|
| Yeni bir pencere açın | (120, 100) |
Move Right düğmesine tıklayın |
(140, 100) |
Show Editor seçeneğini kapatın |
Tuval kaybolur; konum (140, 100) olarak kalır |
Undo düğmesine tıklayın |
Tuval hâlâ gizliyken konum (120, 100) olur |
Redo düğmesine tıklayın |
Konum (140, 100) olur |
Show Editor seçeneğini açın |
Dikdörtgen, redo işleminden sonraki konumunda görünür |
Sürüklemeyi tamamlayın, ardından Undo düğmesine tıklayın |
Dikdörtgen, sürüklemeden önceki konumuna döner |
Düzenleyicinin gizli olduğu kontrolü, model sahipliğini sunumun yaşam süresinden ayırır. Undo yalnızca belirli bir panel mevcutken çalışıyorsa yöneticide kayıtlı hedefi ve bu hedefi yaşatan nesneyi inceleyin. Undo çalışıyor ama redo hiçbir şey yapmıyorsa inverse’in dönüş işlemini kaydedip kaydetmediğini inceleyin. Bir sürükleme birçok undo gerektiriyorsa önizleme güncellemelerinin store’un undo edilebilir değişikliğine ulaşıp ulaşmadığını inceleyin.
Açık düğmeler, değişiklik yolunu incelemeyi kolaylaştırır. Mac’te olağan menü davranışı için standart Edit menüsünü de etkin pencereyle sınayın. AppKit’in temel responder’ı undo yöneticisini bir sonraki responder’dan alır. Pencere delegesinden bir yönetici istediğinde, windowWillReturnUndoManager(_:) belgelenmiş özelleştirme noktasıdır; böyle bir uygulama yoksa pencere kendi yöneticisini oluşturur. İlgisiz bir yöneticiye kayıt yapmak, istenen responder zinciri yönlendirmesini sağlamaz. Apple’ın NSResponder undoManager belgeleri, Apple’ın pencere undo yöneticisi temsilcisi belgeleri.
Store için de bilinçli bir kullanım sonu sınırı gerekir. Store’u, yöneticisi yaşamaya devam ederken değiştirirseniz, store’u serbest bırakmadan önce eylemlerini tutan her yöneticide removeAllActions(withTarget:) kullanarak o store’un kayıtlarını kaldırın. Tuval kaybolduğu için geçmişi temizlemeyin: burada oluşturulan davranış, kaybolma sırasında store’un düzenlemelerinin kullanılabilir kalmasıdır. Apple’ın hedefe özgü undo temizleme belgeleri.
Bu örnekte tek bir sabit dikdörtgen ve silme yolu bulunur. Örneği bir koleksiyona genişletirken kararlı öğe tanımlayıcılarını ve her işlemi geri almak için gereken değerleri yakalayın. Silme işleminin inverse’i, öğeyi ve amaçlanan sıralamasını geri yükleyebilmelidir; koleksiyon değişebiliyorsa yalnızca eski bir dizi indisi yeterli olmaz.
Tamamlanan tasarım, yaşam süresi sorumluluğunu köke, geçici etkileşim durumunu tuvale, geri alınabilir düzenlemeleri ise store’a verir. Tuval gizlendiğinde bu düzenlemeler kullanılabilir kalır, çünkü inverse yöntemleri hâlâ yaşayan store üzerinde çalışır.
Kaynaklar
- registerUndo(withTarget:handler:) — Apple
- StateObject — Apple
- Undo Manager — Apple
- Automatic Reference Counting — Swift.org
- UndoManager — Apple
- undoManager — EnvironmentValues — Apple
- updating(_:body:) — Apple
- onEnded(_:) — Apple
- groupsByEvent — Apple
- undoManager — NSResponder — Apple
- windowWillReturnUndoManager(_:) — Apple
- removeAllActions(withTarget:) — Apple