0

I'm having trouble adopting the more complex invocation-based approach to undo registration in Swift (based on NSHipster article here. Apple's docs still have all sample code in Objective-C, and the semantics are very different for the invocation setup).

My NSDocument subclass Document has the following method that operates on the model objects, which I wish to make undoable:

func rename(object: Any, to newName: String) {
    // This is basically a protocol that requires implementing:
    // var name: String { get set }
    //  
    guard var namedObject = object as? EditorHierarchyDisplayable else {
        return
    }

    // Register undo:
    let undoController = undoManager?.prepare(withInvocationTarget: self) as? Document
    undoController?.rename(object: namedObject, to: namedObject.name)
    undoManager?.setActionName("Rename \(namedObject.localizedClassName)")

    // Perform the change:
    namedObject.name = newName
}

What I have found out is that undoController above is nil, becuase the atempted cast to Document fails. If I remove the cast (and comment out the call to undoController.rename(...), prepare(withInvocationTarget:) returns the folowing object:

(lldb) print undoController
(Any?) $R0 = some {
 payload_data_0 = 0x00006080000398a0
 payload_data_1 = 0x0000000000000000
 payload_data_2 = 0x0000000000000000
 instance_type = 0x000060800024f0d8
}
(lldb) print undoController.debugDescription
(String) $R1 = "Optional(NSUndoManagerProxy)"
(lldb) 

What am I missing?

Nicolas Miari
  • 16,006
  • 8
  • 81
  • 189
  • The documentation of `prepare(withInvocationTarget:)` says "returns self". `self` is `undoManager`. At the bottom of the NSHipster article it says "This article uses Swift version 1.0.". – Willeke Aug 21 '17 at 08:50
  • Yes, so do the docs too. But it's casting the returned value to `as ViewController` (I assume this becomes as `Document` in my case). Also, `as` becomes `as?` in Swift 2+ – Nicolas Miari Aug 21 '17 at 08:53
  • Casting `NSUndoManager` to `Document` is wrong. Swift 1 didn't care but Swift 3 refuses to do it. – Willeke Aug 21 '17 at 09:26
  • So, how do I call my `Document` method `rename(object:to:)` on the returned proxy then? Objective-C lets you send any message to any object (at compile time at least), but Swift is strongly typed... – Nicolas Miari Aug 21 '17 at 09:41
  • The blog post has this line of code: `let undoController : ViewController = undoManager?.prepareWithInvocationTarget(self) as ViewController`. How does this translate to Swift 3? – Nicolas Miari Aug 21 '17 at 09:43
  • 1
    [How do I register NSUndoManager in Swift?](https://stackoverflow.com/questions/24326984/how-do-i-register-nsundomanager-in-swift) – Willeke Aug 21 '17 at 09:51
  • Yes, I saw that question and was already thinking about using the new API `registerUndoWithTarget(target: TargetType, handler: TargetType -> ())`. Thanks. – Nicolas Miari Aug 21 '17 at 09:56

1 Answers1

1

I think the basic confusion is that prepare(withInvocationTarget:) returns a proxy object (that happens to be the undo manager itself, but that's an implementation detail). The idea is that you send this proxy object the same message(s) you send to undo the action, but instead of executing them (because it's not the actual object), it internally captures those invocations and saves them for later.

So your code should really start out something like this:

let selfProxy: Any = undoManager?.prepare(withInvocationTarget: self)

This works great in Objective-C because the "catchall" type (id) has very lax type checking. But the equivalent Any class in Swift is much more stringent and does not lend itself to the same technique, if at all.

See Using NSUndoManager and .prepare(withInvocationTarget:) in Swift 3

James Bucanek
  • 3,299
  • 3
  • 14
  • 30
  • To my surprise, the method used in the answer I linked as "duplicate source", i.e. `(target as AnyObject).methodOfMyTargetClass()` **does** compile without errors or warnings, even though `target` is `AnyObject`, _not_ `MyClass`. I though Swift's strict type system wouldn't allow it to compile. In any case, I moved to the newer, closure-based `registerUndo(withTarget:handler:)`. – Nicolas Miari Aug 25 '17 at 06:01
  • _"This works great in Objective-C because the "catchall" type (id) has very lax type checking."_ - indeed; I used `NSUndoManager` in Objective-C code in the past. – Nicolas Miari Aug 25 '17 at 06:03
  • For some reason I missed the question/answer you linked; thank you. – Nicolas Miari Aug 25 '17 at 06:04