swiftmacosgesturensviewnsundomanager

Undoing and Redoing NSView Object Move Made by NSPanGestureRecognizer


I am working on a sample desktop application to test the UndoManager class, which I don't often get to use. Anyway, the following is the idea.

  1. I create two NSView sub view objects (red & blue). They are added to an IBOutlet-connected NSView object (panView).
  2. I add the pan gesture (NSPanGestureRecognizer) to these two sub view objects.
  3. The applications calls the undo manager when the user moves either sub view object.

The following is my entire code from top to bottom.

 import Cocoa

 class ViewController: NSViewController {
     // MARK: - Variables
     var myView1 = NSView()
     var myView2 = NSView()
     var uuid1 = String()
     var uuid2 = String()


     // MARK: - IBOutlet
     @IBOutlet weak var panView: NSView!


     // MARK: - Life cycle
     override func viewDidLoad() {
         super.viewDidLoad()

         /* myView */
         uuid1 = UUID().uuidString
         myView1 = NSView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 100, height: 100)))
         myView1.identifier = NSUserInterfaceItemIdentifier(rawValue: uuid1)
         myView1.wantsLayer = true
         if let myLayer1 = myView1.layer {
          myLayer1.backgroundColor = NSColor.red.cgColor
         }
         panView.addSubview(myView1)

         uuid2 = UUID().uuidString
         myView2 = NSView(frame: CGRect(origin: CGPoint(x: 400, y: 200), size: CGSize(width: 100, height: 100)))
         myView2.identifier = NSUserInterfaceItemIdentifier(rawValue: uuid2)
         myView2.wantsLayer = true
         if let myLayer2 = myView2.layer {
          myLayer2.backgroundColor = NSColor.blue.cgColor
         }
         panView.addSubview(myView2)

         /* pangesture */
         let panRecognizer1 = NSPanGestureRecognizer.init(target: self, action: #selector(panPictureView(_:)))
         let panRecognizer2 = NSPanGestureRecognizer.init(target: self, action: #selector(panPictureView(_:)))
         myView1.addGestureRecognizer(panRecognizer1)
         myView2.addGestureRecognizer(panRecognizer2)
     }         

     // MARK: - Pan gesture
     @objc func panPictureView(_ sender: NSPanGestureRecognizer) {
         let translation = sender.translation(in: self.view)
         if let movingObject = sender.view {
          let newPosition = CGPoint(x: movingObject.frame.origin.x + translation.x, y: movingObject.frame.origin.y + translation.y)
          movingObject.setFrameOrigin(newPosition)
          sender.setTranslation(CGPoint.zero, in: self.view)
          if sender.state == .began {
              if let rawID = sender.view?.identifier?.rawValue {
                  let dict = ["point": movingObject.frame.origin, "rawID": rawID] as [String : Any]
                  redoMoveObject(dict)
              }
          }
          else if sender.state == .ended {
              if let rawID = sender.view?.identifier?.rawValue {
                  let dict = ["point": movingObject.frame.origin, "rawID": rawID] as [String : Any]
                  undoMoveObject(dict)
              }
          }
         }
     }


     // MARK: - Undoing move
     @objc func undoMoveObject(_ newObject: [String : Any]) {
         undoManager?.registerUndo(withTarget: self, selector: #selector(redoMoveObject(_:)), object: newObject)
         undoManager?.setActionName("Move Object")
         if let point = newObject["point"] as? CGPoint, let rawID = newObject["rawID"] as? String {
          if rawID == uuid1 {
              myView1.frame.origin = point
          }
          else {
              myView2.frame.origin = point
          }
         }
     }

     @objc func redoMoveObject(_ newObject: [String : Any]) {
         undoManager?.registerUndo(withTarget: self, selector: #selector(undoMoveObject(_:)), object: newObject)
         undoManager?.setActionName("Move Object")
         if let point = newObject["point"] as? CGPoint, let rawID = newObject["rawID"] as? String {
          if rawID == uuid1 {
              myView1.frame.origin = point
          }
          else {
              myView2.frame.origin = point
          }
         }
     }
 }

enter image description here

It works. It doesn't crash. It's just that I have to press Command + Z twice to undo the move. And I have to press Command + Shift + Z twice to redo the move. So I don't know where this pause comes from. What am I doing wrong, do you know? Thanks.


Solution

  • Undo is registered twice: at sender.state == .began in redoMoveObject and at sender.state == .ended in undoMoveObject(dict). undoMoveObject and redoMoveObject do the same thing and register each other, they can be merged into one function that registers itself.

    Example:

    // MARK: - Pan gesture
    @objc func panPictureView(_ sender: NSPanGestureRecognizer) {
        if let movingObject = sender.view {
            if sender.state == .began { // register undo before first move
                if let rawID = movingObject.identifier?.rawValue {
                    let dict = ["point": movingObject.frame.origin, "rawID": rawID] as [String : Any]
                    undoMoveObject(dict)
                }
            }
            let translation = sender.translation(in: self.view)
            sender.setTranslation(CGPoint.zero, in: self.view)
            let newPosition = CGPoint(x: movingObject.frame.origin.x + translation.x, y: movingObject.frame.origin.y + translation.y)
            movingObject.setFrameOrigin(newPosition)
        }
    }
    
    // MARK: - Undoing move
    @objc func undoMoveObject(_ newObject: [String : Any]) {
        if let point = newObject["point"] as? CGPoint, let rawID = newObject["rawID"] as? String {
            var movingObject: NSView
            if rawID == uuid1 {
                movingObject = myView1
            }
            else {
                movingObject = myView2
            }
            // register current frame origin for redo/undo
            let dict = ["point": movingObject.frame.origin, "rawID": rawID] as [String : Any]
            undoManager?.registerUndo(withTarget: self, selector: #selector(undoMoveObject(_:)), object: dict)
            undoManager?.setActionName("Move Object")
            // undo/redo
            movingObject.frame.origin = point
        }
    }