Untuk bentuk yang diseret dalam app Mac anda, pratonton pergerakan semasa gerak isyarat dan komitkan kedudukan akhir dalam satu kumpulan undo yang ringkas. Letakkan model dan pengurus undo di bawah satu pemilik, padankan setiap pembukaan kumpulan dengan penutupannya, dan daftarkan pergerakan songsang supaya redo juga berfungsi.
Tutorial ini membina kanvas SwiftUI kecil yang mengandungi satu bulatan sintetik dalam memori. Tugas khususnya ialah memastikan satu seretan yang selesai hanya memerlukan satu undo, tanpa mengira jumlah kemas kini pergerakan yang diterima. Tutorial ini juga menerangkan cara mengekalkan sempadan yang sama dalam paparan AppKit.
Mengapa seretan tidak sepatutnya membiarkan kumpulan undo terbuka
Seretan ialah interaksi yang berlangsung dari semasa ke semasa. Kumpulan undo menghimpunkan operasi yang boleh dibuat asal. Tempoh sempadan ini tidak semestinya sama. Apple mendokumenkan bahawa groupsByEvent secara lalai ialah true dan mengumpulkan operasi mengikut lelaran gelung larian; tingkah laku itu tidak mentakrifkan transaksi seretan yang lengkap. Kumpulan eksplisit juga boleh bersarang di dalam kumpulan sedia ada. Lihat dokumentasi Apple tentang pengumpulan peristiwa automatik dan memulakan kumpulan undo.
Membuka kumpulan dalam panggilan balik pergerakan SwiftUI yang pertama lalu menutupnya dalam onEnded meninggalkan jurang pembatalan. Apple menyatakan dengan jelas bahawa onEnded hanya berjalan apabila gerak isyarat berakhir dengan jayanya. Sebaliknya, keadaan yang dikekalkan melalui updating ditetapkan semula apabila gerak isyarat berakhir atau dibatalkan. endUndoGrouping() yang tidak dipadankan akan mencetuskan pengecualian, jadi meneka sama ada panggilan balik telah membuka kumpulan juga tidak sesuai. Lihat onEnded(_:), updating(_:body:), dan endUndoGrouping().
Untuk kanvas ini, gunakan reka bentuk berikut. Ini ialah pilihan pelaksanaan yang berasaskan sempadan yang didokumenkan itu:
| Peringkat interaksi | Keadaan paparan | Model yang dikomitkan | Pengumpulan undo |
|---|---|---|---|
| Sebelum menyeret | Pusat tersimpan | Pusat asal | Tertutup |
| Kemas kini pergerakan | Pusat asal ditambah translasi sementara | Tidak berubah | Tertutup |
| Selesai dengan jayanya | Pusat akhir | Dikemas kini sekali | Dibuka dan ditutup secara segerak |
| Pembatalan | Pratonton ditetapkan semula | Tidak berubah | Tidak pernah dibuka |
| Undo atau redo | Pusat dipulihkan | Dikemas kini melalui setter boleh balik | Diurus oleh UndoManager |
Pendekatan ini membolehkan bulatan bergerak secara berterusan tanpa menganggap setiap bingkai pratonton sebagai suntingan kekal. Seretan yang selesai menjadi satu operasi model bernama.
Pilih pemilik sejarah undo
Contoh ini memberikan setiap kanvas model dan pengurus undo peribadi sendiri. Oleh sebab pemilik itu mengawal setiap pendaftaran, ia menetapkan groupsByEvent = false sekali semasa pemulaan dan mengumpulkan setiap pergerakan yang dikomitkan secara eksplisit. Apple menghendaki kumpulan ditutup secara eksplisit sebelum undo apabila pengumpulan automatik dinyahdayakan. Contoh ini menutup kumpulan sebelum kaedah komit kembali. Lihat groupsByEvent.
Editor dokumen sedia ada mungkin sudah mempunyai pengurus undo dokumen. SwiftUI mendedahkan pengurus kontekstual melalui @Environment(\.undoManager), dan Apple mendokumenkan bahawa nilainya boleh menjadi nil apabila konteks tidak menyokong undo. Dalam seni bina itu, teruskan menggunakan pengurus dokumen dan dasar pengumpulan yang ditetapkannya; jangan salin konfigurasi pengurus peribadi contoh ini ke pengurus yang dikongsi. Lihat EnvironmentValues.undoManager.
Dengan pengurus yang dikongsi dan pengumpulan automatik didayakan, mendaftarkan satu pergerakan songsang apabila seretan selesai tetap mengelakkan entri sejarah bagi setiap pergerakan. Subkumpulan eksplisit boleh menghimpunkan pergerakan itu, tetapi tidak menjadikannya bebas daripada kumpulan yang membungkusnya. Jika suntingan lain berkongsi kumpulan tersebut, semuanya boleh di-undo bersama. Untuk menjamin transaksi aras atas yang berasingan, pemilik sejarah yang dikongsi perlu menyelaraskannya; paparan anak tidak sepatutnya menutup kumpulan.
Bina contoh SwiftUI yang lengkap
Cipta projek app SwiftUI untuk macOS dengan sasaran penggunaan macOS 14 atau lebih baharu. Gantikan deklarasi app dan paparan kandungan yang dijana dengan dua fail di bawah. Kekalkan hanya satu deklarasi @main. Contoh ini menggunakan butang Undo dan Redo setempat untuk menjelaskan pemilikan; penyepaduan menu app dan rantaian responder ialah langkah penyepaduan yang berasingan.
Model ini menggunakan setter boleh balik: tangkap kedudukan semasa, daftarkan penutupan yang memulihkannya, kemudian gunakan nilai gantian. Apabila penutupan itu dijalankan semasa undo, setter yang sama mendaftarkan songsang sebagai redo. Apple menunjukkan corak pendaftaran timbal balik ini dalam Mendaftarkan Operasi Undo. Dokumentasi semasa juga menandakan UndoManager dan pengendali pendaftarannya sebagai terasing kepada pelaku utama, maka model contoh ini menggunakan @MainActor. Lihat UndoManager.
Letakkan model lengkap berikut dalam ShapeModel.swift:
import Foundation
import Combine
@MainActor
final class ShapeModel: ObservableObject {
@Published private(set) var center = CGPoint(x: 120, y: 100)
@Published private(set) var canUndo = false
@Published private(set) var canRedo = false
private let history: UndoManager
init() {
let manager = UndoManager()
manager.groupsByEvent = false
history = manager
}
func commitDrag(translation: CGSize) {
let destination = CGPoint(
x: center.x + translation.width,
y: center.y + translation.height
)
guard destination != center else { return }
// This owner expects no caller-owned group.
precondition(history.groupingLevel == 0)
history.beginUndoGrouping()
defer {
history.endUndoGrouping()
refreshAvailability()
}
replaceCenter(with: destination)
}
private func replaceCenter(with destination: CGPoint) {
let previous = center
guard previous != destination else { return }
history.registerUndo(withTarget: self) { target in
target.replaceCenter(with: previous)
}
history.setActionName("Move Shape")
center = destination
}
func undo() {
precondition(history.groupingLevel == 0)
guard history.canUndo else { return }
history.undo()
refreshAvailability()
}
func redo() {
precondition(history.groupingLevel == 0)
guard history.canRedo else { return }
history.redo()
refreshAvailability()
}
private func refreshAvailability() {
canUndo = history.canUndo
canRedo = history.canRedo
}
}
defer sengaja berada dalam skop segerak yang sama dengan beginUndoGrouping(). Swift melaksanakan tindakan tertangguh apabila skop itu keluar, termasuk apabila terdapat pemulangan awal atau ralat yang dilemparkan. Ia tidak boleh merentasi panggilan balik gerak isyarat yang dipanggil secara berasingan. Padanan ini memastikan kumpulan model seimbang apabila aliran kawalan biasa keluar daripada skop. Lihat Aliran Kawalan — Tindakan Tertangguh.
Penutupan undo beroperasi pada parameter target, bukannya menangkap self. Apple mendokumenkan bahawa pengurus memegang rujukan tanpa pemilikan kepada sasaran dan mengesyorkan penggunaan parameter yang dibekalkan untuk mengelakkan kitaran pengekalan. Kanvas mengekalkan model selagi ia digunakan, dan model memiliki sejarahnya sendiri. Lihat registerUndo(withTarget:handler:).
Letakkan app dan kanvas dalam ShapeUndoApp.swift:
import SwiftUI
@main
@MainActor
struct ShapeUndoApp: App {
var body: some Scene {
WindowGroup {
ShapeCanvas()
}
}
}
@MainActor
struct ShapeCanvas: View {
@StateObject private var model = ShapeModel()
@GestureState private var previewTranslation: CGSize? = nil
private var drag: some Gesture {
DragGesture(
minimumDistance: 1,
coordinateSpace: .named("board")
)
.updating($previewTranslation) { value, state, _ in
state = value.translation
}
.onEnded { value in
model.commitDrag(translation: value.translation)
}
}
var body: some View {
let translation = previewTranslation ?? .zero
let isDragging = previewTranslation != nil
VStack(spacing: 16) {
ZStack(alignment: .topLeading) {
Color.clear
Circle()
.fill(Color.blue)
.frame(width: 44, height: 44)
.position(
x: model.center.x + translation.width,
y: model.center.y + translation.height
)
.gesture(drag)
}
.frame(width: 420, height: 300)
.background(Color.gray.opacity(0.12))
.coordinateSpace(name: "board")
.clipped()
HStack {
Button("Undo") {
guard previewTranslation == nil else { return }
model.undo()
}
.disabled(isDragging || !model.canUndo)
Button("Redo") {
guard previewTranslation == nil else { return }
model.redo()
}
.disabled(isDragging || !model.canRedo)
}
}
.padding(20)
}
}
Gerak isyarat ini menggunakan satu ruang koordinat kanvas bernama, maka translasi diukur berbanding papan dan bukannya bulatan yang bergerak. Apple mentakrifkan translation sebagai jumlah anjakan dari permulaan gerak isyarat. Tetapkannya pada pratonton; jangan tambahkannya berulang kali pada model semasa pergerakan. Lihat DragGesture dan translation.
Keadaan gerak isyarat pilihan juga menunjukkan sama ada pratonton sedang aktif. Kedua-dua butang sejarah setempat dinyahdayakan semasa pratonton itu. Apabila selesai dengan jayanya, nilai gerak isyarat akhir membekalkan translasi yang dikomitkan. Apabila dibatalkan, keadaan gerak isyarat ditetapkan semula dan pusat yang tersimpan kekal tidak berubah; tiada kumpulan terbuka yang perlu dibersihkan.
Contoh ini sengaja hanya mempunyai satu pihak yang membuat perubahan. Tiada arahan lain mengubah bulatan semasa seretan. Jika anda menambah pergerakan papan kekunci, pemadaman atau arahan penyuntingan lain, tetapkan sama ada setiap arahan tidak tersedia semasa menyeret atau membatalkan pratonton terlebih dahulu. Salurkan semua arahan melalui dasar itu; menyahdayakan dua butang ini sahaja tidak dapat mengawal laluan arahan tambahan.
Sahkan satu seretan, satu undo dan redo yang berfungsi
Gunakan koordinat ilustrasi untuk menyemak pengiraan. Bermula pada (120, 100), translasi terkumpul (10, 5), (25, 12) dan (40, 20) menghasilkan pusat pratonton (130, 105), (145, 112) dan (160, 120). Pusat yang dikomitkan kekal pada (120, 100) sehingga seretan selesai, kemudian berubah sekali kepada (160, 120).
Undo memanggil setter berdaftar dengan (120, 100). Sebelum memulihkannya, setter menangkap (160, 120) dan mendaftarkan kedudukan itu untuk redo. Jangan sekat pendaftaran dalam panggilan balik undo: pendaftaran timbal balik itulah yang memastikan redo kekal tersedia. Apple menerangkan tingkah laku ini dalam Mendaftarkan Operasi Undo.
Kod ini tidak membuat kumpulan eksplisit tambahan di dalam setter boleh balik. UndoManager mengawal pengumpulan semasa memainkan semula sejarah; Apple mendokumenkan bahawa redo() merekodkan operasi yang dimainkan semula pada tindanan undo sebagai satu kumpulan. Nama tindakan ditetapkan melalui setActionName, dengan label sintetik Move Shape. Lihat redo() dan setActionName(_:).
Semak tingkah laku ini dalam app Mac:
- Selesaikan satu seretan dengan beberapa perubahan arah. Satu
Undosepatutnya memulihkan kedudukan permulaannya. - Pilih
Redo. Bentuk itu sepatutnya kembali ke kedudukan akhir seretan yang telah selesai. - Selesaikan dua seretan berasingan. Dua tindakan undo sepatutnya memulihkan dua kedudukan terdahulu mengikut turutan terbalik.
- Tamatkan seretan yang dikenali pada kedudukan permulaannya. Perlindungan tiada perubahan sepatutnya tidak mencipta entri sejarah.
- Uji laluan pembatalan atau pembuangan paparan dalam app yang mengandungi kanvas. Pratonton sepatutnya hilang tanpa pergerakan yang dikomitkan.
- Undo satu pergerakan, kemudian selesaikan pergerakan yang berbeza. Cabang redo yang terdahulu sepatutnya hilang; Apple mendokumenkan bahawa pendaftaran undo baharu akan mengosongkan tindanan redo.
Tingkah laku terakhir ini mengikut Seni bina Undo Manager Apple. Pengiraan koordinat di atas disemak menggunakan pengiraan Python kecil yang deterministik. Kod sumber Swift telah diperiksa tetapi tidak dikompil atau dijalankan di sini kerana persekitaran ini tidak mempunyai pengkompil Swift atau SDK macOS.
Gunakan sempadan yang sama pada kanvas AppKit
Untuk NSView sedia ada, kekalkan urutan peristiwa AppKit. Rujukan semasa Apple mentakrifkan mouseDragged(with:) sebagai pergerakan semasa butang kiri ditekan dan mouseUp(with:) sebagai pelepasan butang itu. Panduan peristiwa arkibnya menerangkan urutan tiga kaedah serta penukaran koordinat yang diperlukan oleh paparan tersuai. Lihat mouseDragged(with:), mouseUp(with:), dan Mengendalikan Peristiwa Tetikus.
Gunakan reka bentuk yang sama dalam kaedah berikut:
- Dalam
mouseDown(with:), uji hit bentuk sintetik dan simpan pusat permulaannya serta lokasi penuding yang ditukar kepada koordinat kanvas. - Dalam
mouseDragged(with:), kira anjakan dari lokasi penuding yang disimpan itu dan lukis semula pratonton sementara. - Dalam
mouseUp(with:), kira anjakan akhir, kosongkan keadaan penjejakan dan panggil kaedah komit segerak model sekali. - Dalam laluan pembatalan atau penguraian pemilik kanvas, buang pratonton dan kosongkan keadaan penjejakan tanpa membuat komit.
Gunakan satu pemilik peristiwa untuk interaksi itu. Jika SwiftUI membenamkan kanvas AppKit, pilih sama ada gerak isyarat SwiftUI atau pengendali tetikus AppKit untuk mengawal pergerakan dan komit. Jika kedua-duanya mendaftarkan pergerakan yang sama, operasi pendua akan terhasil. Pastikan koordinat lukisan konsisten, termasuk sebarang sistem koordinat terbalik atau transformasi kanvas.
Panduan arkib Apple juga menjelaskan bahawa arahan papan kekunci boleh dihantar semasa pendekatan penjejakan tiga kaedah. Oleh itu, sebarang penyepaduan menu mesti mematuhi dasar seretan aktif yang sama seperti butang setempat. Kekalkan sejarah dokumen bersama pemilik dokumennya dan pastikan tindakan menu menggunakan pengurus milik pemilik itu.
Diagnosis kegagalan pengumpulan tanpa menutup kumpulan orang lain
Jika seretan menghasilkan banyak langkah undo, periksa sama ada setter model mendaftar semasa kemas kini pergerakan. Alihkan pendaftaran itu ke komit akhir. Jika beberapa seretan di-undo bersama, periksa dasar pengumpulan automatik pengurus dan sebarang kumpulan yang dibuka oleh pemilik di luarnya. Jika redo tidak tersedia selepas undo, periksa sama ada laluan pemulihan mendaftarkan operasi songsang.
Jika pengumpulan mencetuskan pengecualian, periksa pemilikan sebelum mengubah jumlah panggilan penutupan. Apple mendokumenkan bahawa groupingLevel == 0 bermaksud tiada kumpulan yang terbuka dan bahawa undo() mencetuskan pengecualian apabila lebih daripada satu kumpulan masih terbuka. Gelung yang menutup kumpulan sehingga arasnya mencapai sifar boleh menutup kumpulan milik pemanggil lain. Gunakan aras itu sebagai diagnostik dan kuatkuasakan kontrak pemilik. Lihat groupingLevel dan undo().
Untuk kanvas dalam memori ini, sempadan yang berguna ialah pergerakan yang dikomitkan: pratonton dengan bebas, komit sekali, daftarkan perubahan boleh balik dan tutup kumpulan sebelum kembali. Dengan itu, setiap seretan yang selesai memberikan satu langkah undo sambil mengekalkan pembatalan, penghantaran peristiwa dan pemilikan sejarah dengan jelas.
Sumber
UndoManager — Apple
groupsByEvent — Apple
beginUndoGrouping() — Apple
endUndoGrouping() — Apple
onEnded(_:) — Apple
updating(_:body:) — Apple
undoManager — Apple
Mendaftarkan Operasi Undo — Apple
Aliran Kawalan — Sumber Bahasa Pengaturcaraan Swift — Swift.org
DragGesture — Apple
translation — Apple
redo() — Apple
setActionName(_:) — Apple
Undo Manager — Apple
mouseDragged(with:) — Apple
mouseUp(with:) — Apple
Mengendalikan Peristiwa Tetikus — Apple
groupingLevel — Apple
undo() — Apple