Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-05-31

Using NSUndoManager from Swift

Until Apple releases an undo manager that can accept closures, we will need to use the present NSUndoManager to implement the undo/redo functionality that all users expect. When I tried to use NSUndoManager, I ran into a few problems. Not unsurmountable, but using NSUndoManager is not as simple as some other Cocoa/Foundation classes.

Here are my experiences:

1) The registerUndoWithTarget function pretty much works as advertised. But the selector that is called must be marked with "@objc". That would be fine, but sometimes I want to pass an parameter type that is not known to Objective-C, which won't work because all functions marked with "@objc" must use types that are known to Objective-C. Besides, the parameter must be an object, this means no struct's or enum's.
These two problems caused me to implement a generic Wrapper class and define the @objc func as accepting AnyObject. Example:

class Wrapper<T> {
    var payload: T
    init(_ data: T) {
        self.payload = data
    }

}

enum Action {
    case SET_STRING(String),
}

extension MyClass {
       
    func dispatch(action: Action) -> ActionResult {
        
        let oldString = string
        
        let result = doAction(action)
        
        if result == .CHANGED {
            if let undoManager = g_undoManager {
                undoManager.registerUndoWithTarget(self, selector: "dispatchFromWrapper:", object: Wrapper(Action.SET_STRING(oldString)))
            }
        }
        
        return result
    }
    
    @objc func dispatchFromWrapper(wrapper: AnyObject) {
        dispatch((wrapper as! Wrapper<Action>).payload)
    }


Note 1: When instantiating a Wrapper object, we can use the abbreviated form: Wrapper(Action.SET_STRING(oldString)) instead of the full form: Wrapper<Action>(Action.SET_STRING(oldString)) thanks to type inference.

Note 2: The MyClass definition contains a var called "string" of the type "String".

Don't let the g_undoManager throw you for a loop: each application (normally) uses only one NSUndoManager. The NSUndoManager is part of every NSWindow and NSView. To gain global access to the undoManager I used the following code in the AppDelegate:

// Make the undo manager global

var g_undoManager: NSUndoManager?

class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

    func applicationDidFinishLaunching(aNotification: NSNotification) {
        

        g_undoManager = window.undoManager
...

Perhaps not the best design possible, but it works!

PS: for more on the dispatch pattern that I used here, see: Swift "Dispatch" Design Pattern.

2) The prepareWithInvocationTarget does not seem usable. I tried several variations, including introducing a category on NSUndoManager, but even though some variations were compilable the app always fails at runtime with an "unrecognized selector sent to" error.

Hopefully Apple will bring out a closure oriented approach soon.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment