I'm trying to learn how UndoManager works. I made a small app to Undo/Redo something.
And I have few questions, I cannot find answer in documentation:
I know UndoManager could be accessed in View via
@Environment(\.undoManager) var undoManager
Brilliant. But in this case it's only available in a View, if I want use it somewhere deeper in a structure I have to pass it via Model to Objects... Is a way to access the same UndoManager in other objects? Models, Data... I could be much more convenient, specially if there is many Undo groupings. If I create UndoManager in Document (or somewhere else) it's not visible for main menu Edit -> Undo, Redo
In the app repository on GitHub I implemented Undo/Redo. For me (haha) it looks OK and even works, but not for first action. First action Undo causes Thread 1: signal SIGABRT error. After three actions I can undo two last actions... Bang. Something is wrong
import Foundation
import SwiftUI
struct CustomView: View {
@ObservedObject var model: PointsViewModel
@Environment(\.undoManager) var undoManager
@GestureState var isDragging: Bool = false
@State var dragOffsetDelta = CGPoint.zero
var formatter: NumberFormatter {
let formatter = NumberFormatter()
formatter.allowsFloats = true
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 5
return formatter
}
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 10) {
ForEach(model.insideDoc.points.indices, id:\.self) { index in
HStack {
TextField("X", value: $model.insideDoc.points[index].x, formatter: formatter)
.frame(width: 80, alignment: .topLeading)
TextField("Y", value: $model.insideDoc.points[index].y, formatter: formatter)
.frame(width: 80, alignment: .topLeading)
Spacer()
}
}
Spacer()
}
ZStack {
ForEach(model.insideDoc.points.indices, id:\.self) { index in
Circle()
.foregroundColor(index == model.selectionIndex ? .red : .blue)
.frame(width: 20, height: 20, alignment: .center)
.position(model.insideDoc.points[index])
//MARK: - drag point
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { drag in
if !isDragging {
dragOffsetDelta = drag.location - model.insideDoc.points[index]
model.selectionIndex = index
let now = model.insideDoc.points[index]
undoManager?.registerUndo(withTarget: model, handler: { model in
model.insideDoc.points[index] = now
model.objectWillChange.send()
})
undoManager?.setActionName("undo Drag")
}
model.insideDoc.points[index] = drag.location - dragOffsetDelta
}
.updating($isDragging, body: { drag, state, trans in
state = true
model.objectWillChange.send()
})
.onEnded({drag in model.selectionIndex = index
model.insideDoc.points[index] = drag.location - dragOffsetDelta
model.objectWillChange.send()
})
)
}
}.background(Color.orange.opacity(0.5))
//MARK: - new point
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded{ loc in
let previousIndex = model.selectionIndex
undoManager?.registerUndo(withTarget: model, handler: {model in
model.insideDoc.points.removeLast()
model.selectionIndex = previousIndex
model.objectWillChange.send()
})
model.insideDoc.points.append(loc.location)
model.selectionIndex = model.insideDoc.points.count - 1
model.objectWillChange.send()
}
)
//MARK: - delete point
.onReceive(deleteSelectedObject, perform: { _ in
if let deleteIndex = model.selectionIndex {
let deleted = model.insideDoc.points[deleteIndex]
undoManager?.registerUndo(withTarget: model, handler: {model in
model.insideDoc.points.insert(deleted, at: deleteIndex)
model.objectWillChange.send()
})
undoManager?.setActionName("remove Point")
model.insideDoc.points.remove(at: deleteIndex)
model.objectWillChange.send()
model.selectionIndex = nil
}
})
}
}
}
Any comments about quality of my algorithms will be highly appreciated.
Selecting any option will automatically load the page