UndoManager — how to deal with it?

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:

  1. 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

  1. 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.

Does it crash whatever the first action is, or is the first action always a new point ?

Why don't you setActionName for new point ?

Try to add it and tell what happens.

UndoManager — how to deal with it?
 
 
Q