Untuk editor bentuk Mac dalam memori yang kecil, mulakan dengan syot kilat keadaan berasaskan nilai. Gunakan perintah songsang untuk suntingan yang kerap dan skopnya sempit apabila muatan yang lebih kecil mengimbangi logik tambahan. Kedua-dua pendekatan memerlukan satu pemilik yang stabil dan sempadan komit yang jelas bagi setiap tindakan pengguna.
Panduan ini membincangkan satu tugas: melaksanakan buat asal dan buat semula yang boleh dijangka untuk mengalih, memadam dan menyusun semula bentuk sintetik dalam editor Swift anda sendiri. Rangka kerja keputusan di bawah ialah saranan kejuruteraan yang berasaskan API Apple; Apple tidak menetapkan pilihan universal antara syot kilat dengan perintah.
Apakah yang patut diingati oleh setiap operasi buat asal?
Syot kilat merekodkan keadaan boleh sunting yang terdahulu, seperti tatasusunan bentuk yang tersusun. Buat asal memulihkan tatasusunan itu. Perintah songsang merekodkan operasi yang diperlukan untuk membalikkan suntingan tertentu, seperti menetapkan semula pusat sesuatu bentuk kepada koordinatnya yang terdahulu.
Kedua-dua perwakilan berfungsi melalui UndoManager Foundation. Ia merekodkan operasi pada objek atau pemiliknya; ia tidak memerhatikan tugasan sewenang-wenangnya pada model anda secara automatik. Kaedah mutasi anda mesti mendaftarkan kerja pemulihan yang sesuai. UndoManager
Tentukan keadaan boleh sunting sebelum memilih perwakilan sejarahnya. Bagi editor ini, keadaan itu merangkumi ID bentuk, koordinat dan susunan tatasusunan. Letakkan penunjuk layang dan pratonton seret di luar keadaan tersebut. Tentukan secara jelas sama ada pemilihan termasuk dalam sejarah: memulihkan pemilihan boleh memudahkan pemahaman tentang pemadaman, tetapi ini juga bermakna buat asal mengubah pemilihan semasa.
Gunakan jadual keputusan ini bagi setiap operasi, dan bukannya menghendaki seluruh editor menggunakan satu perwakilan sahaja:
| Operasi atau keadaan | Perwakilan yang disarankan | Maklumat yang perlu disimpan |
|---|---|---|
| Kanvas kecil; beberapa sifat berubah serentak | Syot kilat | Nilai boleh sunting terdahulu yang lengkap |
| Mengalih satu bentuk | Penetap songsang | ID bentuk yang stabil dan pusat terdahulu |
| Memadam satu bentuk | Syot kilat pada mulanya; perintah songsang jika perlu | Untuk perintah songsang: bentuk yang dipadam sepenuhnya dan kedudukan asal |
| Menyusun semula beberapa bentuk | Syot kilat kecuali perintah songsangnya mudah | Susunan terdahulu dan sebarang sifat yang terjejas |
| Kos memori sejarah yang diukur terlalu tinggi | Perintah songsang bersasar | Semua yang diperlukan untuk pemulihan tepat |
Soalan yang berguna ialah: bolehkah anda menyenaraikan semua perkara yang mesti dipulihkan oleh perintah songsang? Jika pemadaman bentuk turut mengubah pemilihan dan susunan, mengingati ID yang dipadam sahaja tidak mencukupi. Syot kilat selalunya lebih mudah diaudit apabila operasi menjejaskan beberapa nilai berkaitan.
Bilakah syot kilat benar-benar bebas?
Struktur dan tatasusunan Swift mempunyai semantik nilai. Oleh itu, tatasusunan bentuk yang dibina sepenuhnya daripada jenis nilai boleh mewakili keadaan terdahulu secara bebas daripada mutasi kemudian. Walau bagaimanapun, menyalin tatasusunan tika kelas mengekalkan rujukan kepada tika tersebut; perubahan pada tika juga boleh mengubah apa yang diperhatikan oleh syot kilat anda. Membalut rujukan itu dalam struktur tidak menghapuskan isu tersebut. Value And Reference Types In Swift
Untuk editor kecil, gunakan data nilai biasa bagi sejarah: pengecam, koordinat, dimensi dan atribut boleh sunting yang lain. Letakkan paparan dan objek pemaparan boleh ubah di luar syot kilat. Jika model langsung menggunakan kelas, bina perwakilan nilai berasingan yang mengandungi setiap atribut yang diperlukan untuk memulihkannya.
Jangan mengandaikan kos memori tetap berdasarkan istilah “syot kilat”. Ukur sejarah yang dikekalkan dan kependaman suntingan menggunakan kanvas sintetik yang mewakili penggunaan sebenar. Sebagai pengiraan muatan ilustratif, 100 entri yang masing-masing mengekalkan 1,000 rekod bentuk mewakili 100,000 kejadian rekod; 100 entri songsang satu bentuk hanya mengekalkan 100 pusat terdahulu berserta pengecam. Bilangan tersebut bukan ukuran bait atau dakwaan tentang peruntukan storan Swift.
Tetapkan kedalaman sejarah dengan sengaja. Apple mendokumenkan bahawa levelsOfUndo mengehadkan kumpulan buat asal aras teratas, dan nilai lalainya, iaitu 0, bermaksud kumpulan tanpa had. Had kumpulan berguna, tetapi ia tidak menetapkan belanjawan bait: syot kilat kelompok dan suntingan satu koordinat boleh mengekalkan jumlah data yang sangat berbeza. levelsOfUndo
Bagaimanakah syot kilat dan perintah songsang mengekalkan buat semula?
Kaedah pemulihan patut mendaftarkan pemulihan yang berlawanan sebelum mengubah model. Apabila buat asal menjalankan kaedah itu, pendaftarannya membekalkan buat semula. Untuk syot kilat, tangkap keadaan yang sedang dipasang sebelum memulihkan keadaan terdahulu. Untuk perintah kedudukan, tangkap pusat semasa sebelum memasang pusat yang disimpan.
Panduan seni bina buat asal Apple yang diarkibkan menerangkan corak pendaftaran timbal balik ini secara jelas: penetap yang dipanggil semasa buat asal mendaftarkan nilai yang diperlukan untuk buat semula. Contoh mereka menggunakan Objective-C; contoh Swift lengkap di bawah menggunakan API closure semasa. Registering Undo Operations
Jangan sekat pendaftaran semata-mata kerana isUndoing bernilai benar. Dalam reka bentuk ini, tindakan itu akan membuang pendaftaran yang menghasilkan buat semula. Elakkan juga daripada memainkan semula pergerakan relatif yang baharu ketika memulihkan kedudukan: menyimpan pusat terdahulu yang tepat menjadikan pemulihan yang dimaksudkan jelas.
Contoh lengkap pada aras model
Program macOS kendiri ini menggabungkan pemadaman berasaskan syot kilat dengan penetap kedudukan songsang. Ia hanya menggunakan nilai sintetik dalam memori dan Foundation, tanpa kebergantungan pada storan kekal atau UI. Simpan sebagai EditorUndo.swift dan kompilkan menggunakan toolchain Swift 6 dan SDK macOS semasa:
swiftc -parse-as-library EditorUndo.swift -o EditorUndo
./EditorUndo
Program ini sengaja memiliki pengurus khusus dan menyahdayakan pengumpulan peristiwa supaya penegasannya tidak bergantung pada gelung peristiwa aplikasi yang sedang berjalan. Setiap suntingan awal menerima kumpulan eksplisit. Kaedah pemulihan tidak membuka kumpulan tambahan semasa buat asal atau buat semula. Apple menghendaki kumpulan ditutup secara eksplisit sebelum buat asal apabila pengumpulan automatik dinyahdayakan. 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")
}
}
Output yang dijangka ialah Undo and redo checks passed. Contoh ini telah diperiksa tetapi tidak dijalankan di sini kerana pengkompil Swift tidak tersedia dalam persekitaran penyelidikan. Contoh ini menunjukkan teras sejarah; antara muka SwiftUI juga memerlukan model atau penyesuai yang boleh diperhatikan untuk menyegarkan paparannya.
Closure ini menggunakan parameter target dan bukannya menangkap editor secara langsung. Apple mendokumenkan bahawa sasaran pendaftaran mestilah jenis rujukan, pengurus menyimpan rujukan tidak dimiliki kepadanya, dan penggunaan parameter closure mengelakkan kitaran pengekalan melalui sasaran yang ditangkap. registerUndo(withTarget:handler:)
Prasyarat mendedahkan andaian model yang rosak dan bukannya menggugurkan operasi buat asal secara senyap. Sempadan perintah pengeluaran patut menolak suntingan tidak sah sebelum pendaftaran dan memaparkan kegagalan pada peringkat aplikasi yang sesuai. ID yang tiada semasa main semula sejarah menunjukkan bahawa sesuatu telah mengubah model di luar jujukan sejarah yang dijangka.
Bagaimanakah seretan patut menjadi satu tindakan yang boleh dibuat asal?
Seretan menghasilkan banyak kemas kini, sedangkan pembaca menjangka satu pergerakan yang selesai boleh dibuat asal. Pengumpulan automatik Apple mengikuti pusingan gelung larian, jadi ia tidak boleh dianggap sebagai takrif keseluruhan gerak isyarat. Kumpulan eksplisit boleh menggabungkan pendaftaran, tetapi sempadannya tetap perlu diurus dengan sengaja. groupsByEvent, beginUndoGrouping()
Untuk editor ini, gunakan jujukan pratonton dan komit:
- Apabila seretan bermula, simpan ID sasaran dan pusat permulaan yang telah dikomit sebagai garis dasar gerak isyarat.
- Gunakan
@GestureStatedanupdating(_:body:)untuk anjakan sementara. Paparkan bentuk pada garis dasarnya ditambah anjakan itu tanpa mengubah item yang telah dikomit. - Apabila
onEnded(_:)berjaya, kira pusat akhir daripada nilai gerak isyarat akhir dan panggil penetap model yang boleh dibuat asal sekali. - Langkau pendaftaran jika pusat hasilnya sama dengan pusat yang telah dikomit.
- Jika dibatalkan, gugurkan pratonton dan kekalkan model yang telah dikomit.
Jujukan ini mengikuti perbezaan yang didokumenkan oleh SwiftUI: keadaan gerak isyarat ditetapkan semula apabila gerak isyarat berakhir atau dibatalkan, manakala onEnded(_:) hanya berjalan apabila gerak isyarat berjaya diselesaikan. Oleh itu, pembersihan yang juga perlu berlaku selepas pembatalan tidak boleh bergantung sepenuhnya pada onEnded(_:). Adding interactivity with gestures
Pastikan keadaan yang telah dikomit dalam editor kekal stabil semasa pratonton itu. Satu dasar mudah ialah menyahdayakan mutasi kanvas lain dan buat asal kanvas semasa seretan aktif, kemudian mendayakannya semula selepas selesai atau dibatalkan. Jika tidak, tentukan cara suntingan yang berlaku di antaranya membatalkan garis dasar tersimpan sebelum menerima komit seretan.
Jika bahagian lain dalam editor perlu memerhatikan koordinat yang telah dikomit dan sentiasa dikemas kini, pemaparan pratonton sahaja mungkin tidak sesuai. Dalam keadaan itu, laksanakan transaksi gerak isyarat yang eksplisit: simpan keadaan permulaan, gunakan kemas kini perantaraan tanpa entri sejarah berasingan, dan daftarkan satu pemulihan apabila berjaya. Pembatalan mesti memulihkan keadaan permulaan. Elakkan daripada membiarkan kumpulan buat asal bersama terbuka merentasi panggilan balik sewenang-wenangnya atau kerja tak segerak.
Siapakah yang patut memiliki model dan pengurus buat asal?
Kekalkan satu pemilik editor yang stabil bagi setiap kanvas boleh sunting yang bebas. Pemilik itu memiliki item yang telah dikomit dan menjadi sasaran operasi sejarah. Kekalkan mutasi model dan interaksi pengurus buat asal pada aktor utama; dokumentasi Apple semasa menandakan UndoManager dan pengendali closurenya sebagai terasing kepada aktor utama. UndoManager
Untuk SwiftUI, baca pengurus paparan menggunakan @Environment(\.undoManager). Salurkan pengurus itu melalui lapisan editor atau perintah yang stabil, dan bukannya mencipta pengurus baharu semasa setiap penilaian paparan. Nilai persekitaran ini adalah pilihan: Apple mendokumenkan bahawa konteks yang tidak disokong mengembalikan nil dan membenarkan pendaftaran buat asal dilangkau dalam keadaan itu. Penyuntingan masih boleh diteruskan, tetapi laluan itu tidak boleh menunjukkan bahawa buat asal tersedia. EnvironmentValues.undoManager
Untuk kanvas AppKit, dapatkan pengurus melalui responder yang sesuai. Pelaksanaan asas NSResponder.undoManager meminta responder seterusnya. Oleh itu, pengurus yang dicipta secara peribadi memerlukan integrasi eksplisit dengan konteks responder kanvas jika tindakan menu standard dijangka mencapainya. Menyimpannya dalam model sahaja tidak mewujudkan sambungan itu. NSResponder.undoManager
Konfigurasi pengumpulan dalam contoh kendiri dimiliki oleh pengurus khususnya. Jangan salin konfigurasi itu kepada pengurus yang dibekalkan persekitaran tanpa memiliki dasar pengumpulan bagi setiap peserta. Dalam UI, daftarkan suntingan kanvas yang telah selesai menggunakan dasar peristiwa sedia ada pada konteks tersebut; kumpulkan beberapa suboperasi segerak apabila semuanya membentuk satu tindakan.
Sebelum melupuskan sasaran editor, buang pendaftaran tertangguhnya pada titik sempadan kitar hayat yang terkawal. Apple menyediakan removeAllActions(withTarget:) untuk membuang operasi buat asal dan buat semula yang ditujukan kepada sasaran itu. Mengosongkan seluruh pengurus juga akan menjejaskan peserta lain jika pengurus itu dikongsi. removeAllActions(withTarget:)
Apakah yang patut anda sahkan sebelum memilih reka bentuk akhir?
Uji jujukan pendek yang merentasi sempadan perwakilan: alih satu bentuk, padam bentuk lain, buat asal kedua-duanya, kemudian buat semula kedua-duanya. Periksa nilai, ID dan susunan selepas setiap langkah. Tambahkan seretan yang dibatalkan, seretan yang berakhir pada kedudukan permulaannya, dan dua tetingkap editor yang bebas. Setiap kes menguji sempadan yang berbeza: data pemulihan, penyelesaian gerak isyarat, pengendalian operasi tanpa kesan atau pemilikan.
Anggap penggantian pukal kanvas sebagai operasi eksplisit. Jadikannya boleh dibuat asal dengan keadaan pemulihan yang mencukupi atau tetapkan garis dasar baharu selepas mengosongkan sejarah lapuk dengan sengaja. Syot kilat yang menggugurkan medan boleh sunting dan perintah songsang yang tidak dapat mencari sasarannya kedua-duanya merupakan pelaksanaan yang tidak lengkap.
Mulakan dengan syot kilat untuk kanvas kecil berasaskan nilai, kekalkan seorang pemilik yang stabil dan komit setiap gerak isyarat sekali. Perkenalkan perintah songsang apabila data pemulihannya jelas dan ukuran membenarkan logik tambahan. Dengan cara itu, perwakilan sejarah boleh berubah tanpa mengubah maksud 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