Untuk editor bentuk Mac kecil yang berjalan di memori, mulailah dengan snapshot status berbasis nilai. Gunakan perintah invers untuk pengeditan yang sering dan cakupannya sempit ketika payload yang lebih kecil sepadan dengan logika tambahan. Kedua pendekatan memerlukan satu pemilik yang stabil dan batas komit yang jelas untuk setiap tindakan pengguna.
Panduan ini membahas satu tugas: menerapkan undo dan redo yang dapat diprediksi untuk memindahkan, menghapus, dan menyusun ulang bentuk sintetis di editor Swift Anda sendiri. Kerangka pengambilan keputusan di bawah ini adalah rekomendasi rekayasa yang diturunkan dari API Apple; Apple tidak menetapkan pilihan snapshot-versus-perintah yang berlaku universal.
Apa yang harus diingat oleh setiap operasi undo?
Snapshot mencatat status yang dapat diedit sebelumnya, seperti array bentuk yang berurutan. Undo memulihkan array tersebut. Perintah invers mencatat operasi yang diperlukan untuk membalikkan pengeditan tertentu, seperti mengatur kembali titik tengah sebuah bentuk ke koordinat sebelumnya.
Kedua representasi bekerja melalui UndoManager milik Foundation. Komponen ini mencatat operasi terhadap suatu objek atau pemiliknya; komponen ini tidak otomatis mengamati penetapan sembarang ke model Anda. Metode mutasi Anda harus mendaftarkan pekerjaan pemulihan yang sesuai. UndoManager
Tentukan status yang dapat diedit sebelum memilih representasi riwayatnya. Untuk editor ini, status tersebut mencakup ID bentuk, koordinat, dan urutan array. Biarkan indikator hover dan pratinjau seret berada di luar status tersebut. Tentukan secara eksplisit apakah pilihan termasuk dalam riwayat: memulihkan pilihan dapat membuat penghapusan lebih mudah dipahami, tetapi itu juga berarti undo mengubah pilihan saat ini.
Gunakan tabel keputusan ini untuk setiap operasi, alih-alih mewajibkan seluruh editor memakai satu representasi:
| Operasi atau kondisi | Representasi yang disarankan | Informasi yang perlu disimpan |
|---|---|---|
| Kanvas kecil; beberapa properti berubah bersama | Snapshot | Nilai lengkap yang dapat diedit sebelumnya |
| Memindahkan satu bentuk | Setter invers | ID bentuk yang stabil dan titik tengah sebelumnya |
| Menghapus satu bentuk | Awalnya snapshot; perintah invers jika diperlukan | Untuk invers: bentuk yang dihapus secara lengkap dan posisi asalnya |
| Menyusun ulang beberapa bentuk | Snapshot kecuali inversnya sederhana | Urutan sebelumnya dan properti apa pun yang terdampak |
| Biaya memori riwayat yang terukur terlalu besar | Perintah invers terarah | Semua yang diperlukan untuk pemulihan secara tepat |
Pertanyaan yang berguna adalah: dapatkah Anda merinci semua hal yang harus dipulihkan oleh invers? Jika penghapusan bentuk juga mengubah pilihan dan urutan, mengingat ID bentuk yang dihapus saja tidak cukup. Snapshot sering kali lebih mudah diaudit ketika operasi memengaruhi beberapa nilai yang saling terkait.
Kapan snapshot benar-benar independen?
Struktur dan array Swift memiliki semantik nilai. Karena itu, array bentuk yang seluruhnya dibangun dari tipe nilai dapat merepresentasikan status sebelumnya secara independen dari mutasi berikutnya. Namun, menyalin array berisi instans kelas hanya mempertahankan referensi ke instans tersebut; perubahan pada instans dapat mengubah apa yang diamati oleh snapshot Anda. Membungkus referensi tersebut dalam struktur tidak menghilangkan masalah itu. Value And Reference Types In Swift
Untuk editor kecil, gunakan data nilai biasa untuk riwayat: pengenal, koordinat, dimensi, dan atribut lain yang dapat diedit. Biarkan tampilan dan objek rendering yang dapat dimutasi berada di luar snapshot. Jika model aktif menggunakan kelas, buat representasi nilai terpisah yang memuat setiap atribut yang diperlukan untuk memulihkannya.
Jangan menyimpulkan biaya memori yang tetap dari kata “snapshot”. Ukur riwayat yang dipertahankan dan latensi pengeditan dengan kanvas sintetis yang representatif. Sebagai contoh perhitungan payload, 100 entri yang masing-masing mempertahankan 1.000 catatan bentuk berarti 100.000 kemunculan catatan; 100 entri invers untuk satu bentuk hanya mempertahankan 100 titik tengah sebelumnya beserta pengenal. Jumlah tersebut bukan ukuran dalam byte yang terukur atau klaim tentang alokasi penyimpanan Swift.
Tetapkan kedalaman riwayat dengan sengaja. Apple mendokumentasikan bahwa levelsOfUndo membatasi grup undo tingkat teratas, dan nilai bawaannya, yaitu nol, berarti grup tidak terbatas. Batas grup berguna, tetapi tidak menetapkan anggaran byte: snapshot batch dan pengeditan satu koordinat dapat mempertahankan jumlah data yang sangat berbeda. levelsOfUndo
Bagaimana snapshot dan perintah invers mempertahankan redo?
Metode pemulihan harus mendaftarkan pemulihan kebalikannya sebelum mengubah model. Ketika undo menjalankan metode tersebut, pendaftarannya menyediakan redo. Untuk snapshot, ambil status yang sedang terpasang sebelum memulihkan status sebelumnya. Untuk perintah posisi, ambil titik tengah saat ini sebelum memasang titik tengah yang tersimpan.
Panduan arsitektur undo Apple yang diarsipkan menjelaskan pola pendaftaran timbal balik ini secara eksplisit: setter yang dipanggil selama undo mendaftarkan nilai yang diperlukan untuk redo. Contohnya menggunakan Objective-C; contoh Swift lengkap di bawah menggunakan API closure saat ini. Registering Undo Operations
Jangan meniadakan pendaftaran hanya karena isUndoing bernilai true. Dalam rancangan ini, tindakan tersebut akan menghapus pendaftaran yang membuat redo. Hindari juga menjalankan ulang gerakan relatif baru saat memulihkan posisi: menyimpan titik tengah sebelumnya secara persis memperjelas pemulihan yang dimaksud.
Contoh lengkap pada tingkat model
Program macOS mandiri ini menggabungkan penghapusan berbasis snapshot dengan setter posisi invers. Program ini hanya menggunakan nilai sintetis dalam memori dan Foundation, tanpa dependensi persistensi atau UI. Simpan sebagai EditorUndo.swift dan kompilasi menggunakan toolchain Swift 6 dan SDK macOS terkini:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Program ini sengaja memiliki manajer khusus dan menonaktifkan pengelompokan peristiwa agar asersinya tidak bergantung pada event loop aplikasi yang sedang berjalan. Setiap pengeditan awal mendapat grup eksplisit. Metode pemulihan tidak membuka grup tambahan selama undo atau redo. Apple mewajibkan grup ditutup secara eksplisit sebelum undo ketika pengelompokan otomatis dinonaktifkan. 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")
}
}
Keluaran yang diharapkan adalah Undo and redo checks passed. Contoh ini telah diperiksa, tetapi tidak dijalankan di sini karena compiler Swift tidak tersedia di lingkungan riset. Contoh ini menunjukkan inti riwayat; antarmuka SwiftUI juga memerlukan model atau adaptor yang dapat diamati agar tampilannya diperbarui.
Closure menggunakan parameter target alih-alih menangkap editor secara langsung. Apple mendokumentasikan bahwa target pendaftaran harus berupa tipe referensi, bahwa manajer menyimpan referensi tanpa kepemilikan ke target tersebut, dan bahwa penggunaan parameter closure menghindari siklus retain melalui target yang ditangkap. registerUndo(withTarget:handler:)
Prasyarat menampakkan asumsi model yang rusak alih-alih diam-diam menghilangkan operasi undo. Batas perintah produksi harus menolak pengeditan yang tidak valid sebelum pendaftaran dan menampilkan kegagalan tingkat aplikasi yang sesuai. ID yang hilang saat pemutaran ulang riwayat menunjukkan bahwa sesuatu telah mengubah model di luar urutan riwayat yang diharapkan.
Bagaimana satu tindakan seret menjadi satu tindakan yang dapat di-undo?
Tindakan seret menghasilkan banyak pembaruan, sementara pengguna mengharapkan satu gerakan yang selesai dapat di-undo. Pengelompokan otomatis Apple mengikuti putaran run loop, sehingga tidak dapat dianggap sebagai definisi seluruh gestur. Grup eksplisit dapat menggabungkan pendaftaran, tetapi batasnya tetap perlu dikelola dengan sengaja. groupsByEvent, beginUndoGrouping()
Untuk editor ini, gunakan urutan pratinjau dan komit:
- Saat tindakan seret dimulai, simpan ID target dan titik tengah awal yang sudah dikomit sebagai patokan gestur.
- Gunakan
@GestureStatedanupdating(_:body:)untuk translasi sementara. Render bentuk pada posisi patokan ditambah translasi tersebut tanpa mengubah item yang sudah dikomit. - Pada
onEnded(_:)yang berhasil, hitung titik tengah akhir dari nilai gestur terakhir lalu panggil setter model yang dapat di-undo satu kali. - Lewati pendaftaran jika titik tengah hasilnya sama dengan titik tengah yang sudah dikomit.
- Jika dibatalkan, buang pratinjau dan pertahankan model yang sudah dikomit.
Urutan ini mengikuti pembedaan yang didokumentasikan SwiftUI: status gestur direset saat gestur berakhir atau dibatalkan, sedangkan onEnded(_:) hanya dijalankan ketika gestur selesai dengan sukses. Karena itu, pembersihan yang juga harus terjadi setelah pembatalan tidak bisa hanya bergantung pada onEnded(_:). Adding interactivity with gestures
Jaga agar status kanvas yang sudah dikomit tetap stabil selama pratinjau tersebut. Salah satu kebijakan sederhana adalah menonaktifkan mutasi kanvas lain dan undo kanvas saat seret sedang aktif, lalu mengaktifkannya kembali setelah selesai atau dibatalkan. Jika tidak, tentukan bagaimana pengeditan yang terjadi di antaranya membatalkan patokan yang disimpan sebelum menerima komit seret.
Jika bagian editor lain harus mengamati koordinat yang sudah dikomit dan terus diperbarui, rendering yang hanya memakai pratinjau mungkin tidak cocok. Dalam hal itu, terapkan transaksi gestur eksplisit: simpan status awal, terapkan pembaruan antara tanpa entri riwayat terpisah, dan daftarkan satu pemulihan saat berhasil. Pembatalan harus memulihkan status awal. Hindari membiarkan grup undo bersama tetap terbuka melewati callback sembarang atau pekerjaan asinkron.
Siapa yang seharusnya memiliki model dan manajer undo?
Pertahankan satu pemilik editor yang stabil untuk setiap kanvas yang dapat diedit secara independen. Pemilik ini memiliki item yang sudah dikomit dan menjadi target operasi riwayat. Jalankan mutasi model dan interaksi undo pada aktor utama; dokumentasi Apple saat ini menandai UndoManager dan handler closure-nya sebagai terisolasi pada aktor utama. UndoManager
Untuk SwiftUI, baca manajer tampilan menggunakan @Environment(\.undoManager). Berikan manajer itu melalui lapisan editor atau perintah yang stabil, alih-alih membuat manajer baru pada setiap evaluasi tampilan. Nilai lingkungan bersifat opsional: Apple mendokumentasikan bahwa konteks yang tidak mendukungnya mengembalikan nil dan mengizinkan pendaftaran undo dilewati dalam kondisi tersebut. Pengeditan tetap dapat dilakukan, tetapi jalur itu tidak boleh menyatakan bahwa undo tersedia. EnvironmentValues.undoManager
Untuk kanvas AppKit, cari manajer melalui responder yang sesuai. Implementasi dasar NSResponder.undoManager meminta manajer ke responder berikutnya. Karena itu, manajer yang dibuat secara privat memerlukan integrasi eksplisit dengan konteks responder kanvas jika tindakan menu standar diharapkan dapat menjangkaunya. Menyimpannya di model saja tidak membangun koneksi tersebut. NSResponder.undoManager
Konfigurasi pengelompokan pada contoh mandiri berlaku untuk manajer khususnya. Jangan menyalin konfigurasi itu ke manajer yang disediakan lingkungan kecuali Anda memiliki kebijakan pengelompokan untuk setiap pihak yang terlibat. Dalam UI, daftarkan pengeditan kanvas yang telah selesai menggunakan kebijakan peristiwa yang sudah ada di konteks; kelompokkan beberapa suboperasi sinkron ketika semuanya merupakan satu tindakan.
Sebelum membuang target editor, hapus pendaftaran yang tertunda untuk target tersebut pada batas siklus hidup yang terkendali. Apple menyediakan removeAllActions(withTarget:) untuk menghapus operasi undo dan redo yang ditujukan ke target tersebut. Mengosongkan seluruh manajer juga akan memengaruhi pihak lain jika manajer dipakai bersama. removeAllActions(withTarget:)
Apa yang harus diverifikasi sebelum memilih rancangan akhir?
Uji urutan singkat yang melintasi batas representasi: pindahkan satu bentuk, hapus bentuk lain, undo keduanya, lalu redo keduanya. Periksa nilai, ID, dan urutan setelah setiap langkah. Tambahkan seret yang dibatalkan, seret yang berakhir di posisi awal, serta dua jendela editor independen. Setiap kasus menguji batas yang berbeda: data pemulihan, penyelesaian gestur, penanganan tanpa operasi, atau kepemilikan.
Perlakukan penggantian massal kanvas sebagai operasi eksplisit. Jadikan operasi itu dapat di-undo dengan status pemulihan yang memadai atau tetapkan patokan baru setelah sengaja menghapus riwayat yang sudah usang. Snapshot yang menghilangkan bidang yang dapat diedit dan perintah invers yang tidak dapat menemukan targetnya sama-sama merupakan implementasi yang tidak lengkap.
Mulailah dengan snapshot untuk kanvas kecil berbasis nilai, pertahankan satu pemilik yang stabil, dan komit setiap gestur satu kali. Perkenalkan perintah invers ketika data pemulihannya jelas dan hasil pengukuran membenarkan logika tambahan. Dengan begitu, representasi riwayat dapat berubah tanpa mengubah makna satu tindakan pengguna.
Sumber
- 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