Daftarkan operasi undo pada penyimpanan model yang berumur panjang, dan biarkan tampilan mengirimkan perubahan melalui metode penyimpanan. Untuk editor bentuk Mac, simpan pratinjau seret secara lokal, lakukan komit sekali saat dilepas, dan biarkan penyimpanan mendaftarkan inversi yang juga memungkinkan redo.
Tutorial ini membuat aplikasi SwiftUI macOS kecil yang berisi satu persegi panjang sintetis. Anda dapat memindahkannya, menyembunyikan editornya, membatalkan pemindahan, lalu menampilkan editornya kembali. Tugas konkretnya adalah mempertahankan perilaku undo saat tampilan pengeditan menghilang sementara modelnya tetap hidup. Semuanya tetap berada di memori.
Mengapa penyimpanan harus menerima operasi undo?
Metode pendaftaran berbasis closure milik Apple memerlukan target bertipe referensi. Dokumentasi Apple juga menyatakan bahwa undo manager menyimpan referensi tanpa kepemilikan ke target tersebut. Karena itu, mendaftarkan sebuah aksi tidak menetapkan masa hidup target. Apple menyarankan penggunaan parameter target milik handler alih-alih menangkap referensi lain ke target yang sama. Dokumentasi pendaftaran undo Apple.
Penyimpanan yang memiliki state yang diedit merupakan target yang sesuai. Pemilik yang stabil menjaga penyimpanan tetap hidup; tampilan sementara menampilkannya dan meminta perubahan. Di sini, tampilan root jendela memiliki penyimpanan dengan @StateObject, sementara kanvas yang dapat dihapus mengamatinya. SwiftUI mempertahankan objek state selama masa hidup kontainer yang mendeklarasikannya, tetapi mengubah identitas kontainer dapat membuat instans baru. Dokumentasi StateObject Apple.
“Inversi milik penyimpanan” menjelaskan tempat logika pembalikan berada. Undo manager tetap mencatat dan menjadwalkan callback. Penyimpanan menyediakan nilai lama dan metode yang memulihkannya. Pembagian ini mengikuti arsitektur yang didokumentasikan Apple, yang memungkinkan penerima undo berupa objek yang diubah atau pemiliknya. Panduan Undo Manager Apple.
| Tanggung jawab | Pemilik dalam contoh ini |
|---|---|
| Posisi persegi panjang yang telah diubah | Penyimpanan model |
| Pendaftaran inversi dan mutasi | Metode penyimpanan model |
| Masa hidup penyimpanan | Root jendela yang stabil |
| Translasi seret sementara | State gestur kanvas |
| Riwayat dan eksekusi undo | Undo manager dari environment |
| Memilih manager yang berlaku | Tampilan pada batas interaksi |
Terapkan mutasi penyimpanan yang dapat dibalik
Buat aplikasi SwiftUI macOS dengan target deployment macOS 13 atau lebih baru dan Apple SDK terkini. Tiga berkas berikut membentuk contoh lengkap; gantikan titik masuk aplikasi dan tampilan konten yang dibuat otomatis alih-alih menyimpan deklarasi @main kedua. Compiler Swift dan framework macOS tidak tersedia di lingkungan riset ini, jadi contoh ini sudah diperiksa tetapi belum dikompilasi di sini.
Buat ShapeStore.swift. Penyimpanan memiliki satu posisi yang telah diubah dan satu metode mutasi. Menangkap posisi sebelumnya sebelum penetapan memberi metode itu informasi yang dibutuhkan untuk membalikkan perubahan.
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
}
}
Inversi memanggil metode yang sama. Misalkan posisi berubah dari (120, 100) menjadi (160, 100). Callback yang didaftarkan meminta (120, 100). Saat undo menjalankan callback tersebut, metode melihat (160, 100) sebagai nilai sebelumnya dan mendaftarkan operasi untuk kembali. Apple mendokumentasikan bahwa pendaftaran yang dibuat saat undo berlangsung menjadi operasi redo. Melewatkan pendaftaran setiap kali isUndoing bernilai true akan merusak pola ini. Panduan Undo Manager Apple.
Closure bekerja dengan parameter store dan menangkap manager secara lemah. Ini mencegah closure tersimpan milik manager membuat referensi kuat kembali ke manager tersebut. Dokumentasi penghitungan referensi Swift menjelaskan bahwa referensi lemah tidak menjaga instans yang dirujuk tetap hidup dan menjadi nil saat instans itu dibatalkan alokasinya. The Swift Programming Language: Automatic Reference Counting.
Penyimpanan dan metode mutasinya diisolasi ke aktor utama. Ini sesuai dengan dokumentasi Apple terkini: UndoManager dan handler pendaftaran diisolasi ke aktor utama. Metode ini melakukan pendaftaran dan mutasi secara sinkron, sehingga perubahan kecil ini tetap berada dalam interaksi yang memanggilnya. Dokumentasi UndoManager Apple.
Tempatkan penyimpanan di atas editor yang dapat dihapus
Buat StoreUndoDemoApp.swift. Root memiliki penyimpanan terlepas dari apakah kanvas sedang terlihat. Tombol-tombolnya membaca manager environment saat suatu aksi dijalankan. Kanvas menerima manager yang sama untuk perubahan yang dilakukannya.
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)
}
}
Undo manager SwiftUI dari environment bersifat opsional. Apple secara eksplisit mengizinkan pendaftaran undo dilewati ketika environment menyediakan nil. Contoh ini tetap memindahkan persegi panjang dalam situasi tersebut, tetapi tidak membuat entri undo. Jika operasi undo yang diharapkan tidak ada, periksa apakah interaksi menerima manager sebelum mengubah logika inversi. Dokumentasi undoManager environment Apple.
Meneruskan manager ke setiap mutasi juga membuat konteks jendela tetap eksplisit. Penyimpanan tidak mempertahankan manager jendela mana pun yang kebetulan muncul lebih dahulu. Untuk contoh ini, root setiap jendela memiliki penyimpanannya sendiri. Jika kelak beberapa jendela mengedit model bersama, tentukan riwayat mana yang memiliki perubahan tersebut sebelum menggunakan kembali pengaturan ini di beberapa jendela.
Komit satu kali seret tanpa memperpanjang grup undo melintasi peristiwa
Buat ShapeCanvas.swift. Selama diseret, posisi yang ditampilkan untuk persegi panjang mencakup translasi sementara. Posisi penyimpanan yang telah diubah hanya berganti saat gestur berhasil berakhir. Selesaikan seretan sebelum menjalankan perubahan lain atau perintah undo; contoh ringkas ini mengasumsikan interaksi berurutan dan tidak mengatur perintah simultan selama seretan.
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()
}
}
Gestur menggunakan translasi akhirnya untuk mengomit pemindahan. Gestur ini tidak bergantung pada waktu reset properti pratinjau. Apple mendokumentasikan bahwa updating(_:body:) mereset state gestur saat gestur berakhir atau dibatalkan, sedangkan onEnded(_:) hanya dijalankan setelah penyelesaian berhasil. Karena itu, gestur yang dibatalkan dalam rancangan ini tidak menghasilkan pemindahan yang tersimpan atau aksi undo baru. Dokumentasi pembaruan gestur Apple, dokumentasi penyelesaian gestur Apple.
Ini mempertahankan tiga batas terpisah: pembaruan pratinjau mengubah state tampilan sementara; penyelesaian yang berhasil meminta satu perubahan model; penyimpanan mendaftarkan inversi perubahan tersebut. Memanggil mutasi penyimpanan pada setiap pembaruan seret justru akan mendaftarkan posisi antara sepanjang gestur.
Pengelompokan peristiwa default milik Apple mengumpulkan pendaftaran di sekitar proses run loop. Seretan dapat menghasilkan pembaruan dalam beberapa proses, sehingga pengelompokan peristiwa saja tidak mewakili transaksi untuk keseluruhan seretan yang dimaksud. Di sini, komit satu kali menghindari masalah itu tanpa mengubah konfigurasi manager bersama. Dokumentasi groupsByEvent Apple.
Untuk perintah mendatang yang mengubah beberapa item secara sinkron, grup undo eksplisit yang seimbang dapat mengumpulkan inversinya. Awal dan akhirnya harus tetap berada di dalam perintah itu. Jika mutasi model langsung selama seretan diperlukan, rancang operasi mulai, pembaruan, komit, dan batal secara terpisah, termasuk cara perintah lain berinteraksi dengan pengeditan yang sedang berlangsung. Pendekatan pratinjau saja di sini sengaja tidak memasukkan mekanisme transaksi tambahan tersebut ke dalam penyimpanan.
Verifikasi kepemilikan, redo, dan perutean jendela secara terpisah
Gunakan urutan berikut pada aplikasi yang sedang berjalan. Koordinatnya merupakan nilai perkiraan yang diharapkan berdasarkan state awal dan kenaikan tombol yang diberikan, bukan hasil pengukuran.
| Aksi | Posisi atau hasil tersimpan yang diharapkan |
|---|---|
| Buka jendela baru | (120, 100) |
Klik Move Right |
(140, 100) |
Nonaktifkan Show Editor |
Kanvas menghilang; posisi tetap (140, 100) |
Klik Undo |
Posisi menjadi (120, 100) saat kanvas tetap tidak terlihat |
Klik Redo |
Posisi menjadi (140, 100) |
Aktifkan Show Editor |
Persegi panjang muncul pada posisi hasil redo |
Selesaikan seretan, lalu klik Undo |
Persegi panjang kembali ke posisinya sebelum seretan tersebut |
Pemeriksaan editor tersembunyi membedakan kepemilikan model dari masa hidup presentasi. Jika undo hanya berfungsi saat panel tertentu ada, periksa target yang didaftarkan pada manager dan objek yang mempertahankannya. Jika undo berfungsi tetapi redo tidak melakukan apa pun, periksa apakah inversi mendaftarkan operasi untuk kembali. Jika satu seretan memerlukan banyak undo, periksa apakah pembaruan pratinjau mencapai mutasi penyimpanan yang dapat di-undo.
Tombol eksplisit memudahkan pemeriksaan jalur mutasi. Untuk perilaku menu Mac biasa, uji juga menu Edit standar dengan jendela yang dimaksud sedang aktif. Responder dasar AppKit memperoleh undo manager dari responder berikutnya. Saat sebuah jendela meminta manager kepada delegatnya, windowWillReturnUndoManager(_:) adalah titik kustomisasi yang didokumentasikan; tanpa implementasi tersebut, jendela membuat managernya sendiri. Mendaftar pada manager yang tidak terkait tidak menetapkan perutean yang dimaksud melalui rantai responder. Dokumentasi undoManager NSResponder Apple, dokumentasi delegat undo manager jendela Apple.
Penyimpanan juga membutuhkan batas penghentian yang direncanakan. Jika Anda mengganti penyimpanan sementara managernya tetap ada, hapus pendaftaran milik penyimpanan tersebut sebelum melepasnya dengan menggunakan removeAllActions(withTarget:) pada setiap manager yang menyimpan aksinya. Jangan hapus riwayat hanya karena kanvas menghilang: perilaku yang sedang dibuat adalah mempertahankan perubahan penyimpanan agar tetap tersedia selama kanvas menghilang. Dokumentasi pembersihan undo khusus target Apple.
Contoh ini memiliki satu persegi panjang tetap dan tidak memiliki jalur penghapusan. Saat memperluasnya menjadi koleksi, tangkap pengenal item yang stabil dan nilai-nilai yang diperlukan untuk membalikkan setiap operasi. Penghapusan memerlukan inversi yang dapat memulihkan item dan urutannya; indeks array lama saja tidak cukup jika koleksi dapat berubah.
Rancangan yang selesai memberi root tanggung jawab atas masa hidup, kanvas tanggung jawab atas state interaksi sementara, dan penyimpanan tanggung jawab atas perubahan yang dapat dibalik. Menyembunyikan kanvas membuat perubahan tersebut tetap tersedia karena metode inversinya bekerja pada penyimpanan yang masih hidup.
Sumber
- 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