A Swiftlier Undo


After struggling to get NSUndoManager to work correctly in a Swift-only app under OS X 10.11 El Capitan, I finally figured it out (and it works the same for iOS 9). The meat is in answer I posted to a StackOverflow question. Reposted below for completeness.


OS X 10.11 and iOS 9 introduce a new NSUndoManager function:

public func registerUndoWithTarget<TargetType>(target: TargetType, handler: TargetType -> ())

Example

Imagine a view controller (self in this example, of type MyViewController) and a Person model object with a stored property name.

func setName(name: String, forPerson person: Person) {
	
	// Register undo
	undoManager?.registerUndoWithTarget(self, handler: { [oldName = person.name] (MyViewController) -> (target) in
		
		target.setName(oldName, forPerson: person)
		
	})
	
	// Perform change
	person.name = name
	
	// ...
	
}

Caveat

If you’re finding your undo isn’t (ie, it executes but nothing appears to have happened, as if the undo operation ran but it’s still showing the value you wanted to undo from), consider carefully what the value (the old name in the example above) actually is at the time the undo handler closure is executed.

Any old values to which you want to revert (like oldName in this example) must be captured as such in a capture list. That is, if the closure’s single line in the example above were instead:

target.setName(person.name, forPerson: person)

…undo wouldn’t work because by the time the undo handler closure is executed, person.name is set to the new name, which means when the user performs an undo, your app (in the simple case above) appears to do nothing since it’s setting the name to its current value, which of course isn’t undoing anything.

The capture list ([oldName = person.name]) ahead of the signature ((MyViewController) -> ()) declares oldName to reference person.name as it is when the closure is declared, not when it’s executed.

More Information About Capture Lists

For more information about capture lists, there’s a great article by Erica Sadun titled Swift: Capturing references in closures. It’s also worth paying attention to the retain cycle issues she mentions. Also, though she doesn’t mention it in her article, inline declaration in the capture list as I use it above comes from the Expressions section of the Swift Programming Language book for Swift 2.0.

Other Ways

Of course, a more verbose way to do it would be to let oldName = person.name ahead of your call to registerUndoWithTarget(_:handler:), then oldName is automatically captured in scope. I find the capture list approach easier to read, since it’s right there with the handler.

I also completely failed to get registerWithInvocationTarget() to play nice with non-NSObject types (like a Swift enum) as arguments. In the latter case, remember that not only should the invocation target inherit from NSObject, but the arguments to the function you call on that invocation target should as well. Or at least be types that bridge to Cocoa types (like String and NSString or Int and NSNumber, etc.). But there were also problems with the invocation target not being retained that I just couldn’t solve. Besides, using a closure as a completion handler is far more Swiftly.

In Closing (Get it?)

Figuring all this out took me several hours of barely-controlled rage (and probably some concern on the part of my Apple Watch about my heart rate - “tap-tap! dude… been listening to your heart and you might want to meditate or something”). I hope my pain and sacrifice helps. :-)