How do I register UndoManager in Swift?

2020-02-05 03:48发布

How do I use UndoManager (previously NSUndoManager) in Swift?

Here's an Objective-C example I've tried to replicate:

[[undoManager prepareWithInvocationTarget:self] myArgumentlessMethod];

Swift, however, seems to not have NSInvocation, which (seemingly) means I can't call methods on the undoManager that it doesn't implement.

I've tried the object-based version in Swift, but it seems to crash my Playground:

undoManager.registerUndoWithTarget(self, selector: Selector("myMethod"), object: nil)

However it seems to crash, even with my object accepts an argument of type AnyObject?

What's the best way to do this in Swift? Is there a way to avoid sending an unnecessary object with the object-based registration?

7条回答
放荡不羁爱自由
2楼-- · 2020-02-05 04:10

I too have done a bit of reading and came up with the following: I have 2 tableViews, source by a dictionary and array controller for playlists and its items respectively, which I'm adding to the Helium 3 project on GitHub (not there yet); here's a preview:

dynamic var playlists = Dictionary<String, Any>()
dynamic var playCache = Dictionary<String, Any>()

//  MARK:- Undo keys to watch for undo: dictionary(list) and play item
var listIvars : [String] {
    get {
        return ["key", "value"]
    }
}
var itemIvars : [String] {
    get {
        return ["name", "temp", "time", "rank", "rect", "label", "hover", "alpha", "trans"]
    }
}

internal func observe(_ item: AnyObject, keyArray keys: [String], observing state: Bool) {
    switch state {
    case true:
        for keyPath in keys {
            item.addObserver(self, forKeyPath: keyPath, options: [.old,.new], context: nil)
        }
        break
    case false:
        for keyPath in keys {
            item.removeObserver(self, forKeyPath: keyPath)
        }
    }
}

//  Start or forget observing any changes
internal func observing(_ state: Bool) {
    for dict in playlists {
        let items: [PlayItem] = dict.value as! [PlayItem]
        self.observe(dict as AnyObject, keyArray: listIvars, observing: state)
        for item in items {
            self.observe(item, keyArray: itemIvars, observing: state)
        }
    }
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let undo = self.undoManager {
        let oldValue = change?[NSKeyValueChangeKey(rawValue: "old")]
        let newValue = change?[NSKeyValueChangeKey(rawValue: "new")]

        undo.registerUndo(withTarget: self, handler: {[oldVals = ["key": keyPath!, "old": oldValue as Any] as [String : Any]] (PlaylistViewController) -> () in

            (object as AnyObject).setValue(oldVals["old"], forKey: oldVals["key"] as! String)
            if !undo.isUndoing {
                undo.setActionName(String.init(format: "Edit %@", keyPath!))
            }
        })
        Swift.print(String.init(format: "%@ %@ -> %@", keyPath!, oldValue as! CVarArg, newValue as! CVarArg))
    }
}
override func viewWillAppear() {
    //  Start observing any changes
    observing(true)
}

override func viewDidDisappear() {
    //  Stop observing any changes
    observing(false)
}

//  "List" items are controller objects - NSDictionaryControllerKeyValuePair
internal func addList(_ item: NSDictionaryControllerKeyValuePair, atIndex index: Int) {
    if let undo = self.undoManager {
        undo.registerUndo(withTarget: self, handler: {[oldVals = ["item": item, "index": index] as [String : Any]] (PlaylistViewController) -> () in
            self.removeList(oldVals["item"] as! NSDictionaryControllerKeyValuePair, atIndex: oldVals["index"] as! Int)
            if !undo.isUndoing {
                undo.setActionName("Add PlayList")
            }
        })
    }
    observe(item, keyArray: listIvars, observing: true)
    playlistArrayController.insert(item, atArrangedObjectIndex: index)


    DispatchQueue.main.async {
        self.playlistTableView.scrollRowToVisible(index)
    }
}
internal func removeList(_ item: NSDictionaryControllerKeyValuePair, atIndex index: Int) {
    if let undo = self.undoManager {
        undo.prepare(withInvocationTarget: self.addList(item, atIndex: index))
        if !undo.isUndoing {
            undo.setActionName("Remove PlayList")
        }
    }
    if let undo = self.undoManager {
        undo.registerUndo(withTarget: self, handler: {[oldVals = ["item": item, "index": index] as [String : Any]] (PlaylistViewController) -> () in
            self.addList(oldVals["item"] as! NSDictionaryControllerKeyValuePair, atIndex: oldVals["index"] as! Int)
            if !undo.isUndoing {
                undo.setActionName("Remove PlayList")
            }
        })
    }
    observe(item, keyArray: listIvars, observing: false)
    playlistArrayController.removeObject(item)

    DispatchQueue.main.async {
        self.playlistTableView.scrollRowToVisible(index)
    }
}

"List" items are NSDictionaryControllerKeyValuePair for the NSDictionaryController.

The "item" handling is a bit more complicated but this should get you going. Each time a list or item is added/removed the proper the add|remove method is called. Then you start observing as new items appear and forget as they're removed, this also observes each object's ivars for changes.

Enjoy.

查看更多
登录 后发表回答