What is the proper use of UIResponder.undoManager (Swift 4)

I'm a newbie with Swift and iOS, so be gentle, please. 😉 (I have 20+ years of writing GUI-based Java apps and extensive design background with C++, so I'm not a newbie at programming in general.) I hope I haven't provided too much detail here. I'm trying to be concise, but also trying to explain what bases I've already covered.


I'm try to parse the documentation for UIResponder.undoManager found here because it doesn't seem to be working for me: https://developer.apple.com/documentation/uikit/uiresponder/1621122-undomanager


It sounds like there's a runtime stack of undoManager objects maintained by the UIResponder code (a superclass of UIView and thus part of all views). This stack should be searchable by simply invoking self.undoManager:


"When you request an undo manager, the request goes up the responder chain and the

UIWindow
object returns a usable instance."


So if I have a view controller that is at the top of a UINavigationController stack (i.e. is the currently visible view), I should be able to access

self.undoManager and that will invoke my superclass's computed property, which walks the runtime stack looking for the relevant UndoManager object.


I'm not seeing that behavior. Here's my structure:


UITabBarController at the top (here is where I override undoManager and return a private UndoManager object; I also override canBecomeFirstResponder to return true)


viewDidAppear() calls beginFirstResponder() (which, as I understand it, adds the current object to the runtime stack, described above)


-- UINavigationController (activated by storyboard segue for the currently selected tab)


-- -- UITableViewController (root view controller for the nav controller; this is where I access self.undoManager)


When I breakpoint in my tab bar controller, I can see the private UndoManager object being created and I see my computer property's get method being called. But when the table view controller access undoManager (inherited from UIResponder), it always returns nil.


It looks like the UIResponder property isn't searching the runtime stack properly, or maybe there's some other method that I need to call or implement to activate all of this. I was hoping I could find the source code for UIResponder somewhere (part of Swift being open source, right?) but I can't find any of the library code. I'm guessing Apple is doing the same thing that Microsoft did with C# -- supporting the language on other platforms, but not the libraries?


I appreciate any insight you might have! Thanks!

Accepted Reply

The reason undoManager is nil is that the next responder is nil—indicating that your view controller is not part of the responder chain. I modified your callbackFunction() method to show this (I added lines 3 and 4 in the following snippet):

    func callbackFunction(_ msg: String) -> Void {
        let typ = "MyTableViewVC"
        print("\(typ).callbackFunction next responder: \(next)")
        print("\(typ).callbackFunction view.superview: \(view.superview)")
        if let _ = self.undoManager {
            print("\(typ).callbackFunction: Found in 'self' \(msg)")
        } else {
            print("\(typ).callbackFunction: NOT found in 'self' \(msg)")
        }
        if let _ = self.view.undoManager {
            print("\(typ).callbackFunction: Found in 'self.view' \(msg)")
        } else {
            print("\(typ).callbackFunction: NOT found in 'self.view' \(msg)")
        }
    }


Output (just from that method):

MyTableViewVC.callbackFunction next responder: nil
MyTableViewVC.callbackFunction view.superview: nil
MyTableViewVC.callbackFunction: NOT found in 'self' via closure
MyTableViewVC.callbackFunction: NOT found in 'self.view' via closure
MyTableViewVC.callbackFunction next responder: nil
MyTableViewVC.callbackFunction view.superview: nil
MyTableViewVC.callbackFunction: NOT found in 'self' via delegate
MyTableViewVC.callbackFunction: NOT found in 'self.view' via delegate


So, why is the next responder nil? From the documentation of UIResponder.next (bold added for emphasis):

The UIResponder class does not store or set the next responder automatically, so this method returns nil by default. Subclasses must override this method and return an appropriate next responder. For example, UIView implements this method and returns the UIViewController object that manages it (if it has one) or its superview (if it doesn’t). UIViewController similarly implements the method and returns its view’s superview. UIWindow returns the application object. The shared UIApplication object normally returns nil, but it returns its app delegate if that object is a subclass of UIResponder and has not already been called to handle the event.

Note from the output above that the view controller's view's superview is nil also. This is because a view controller's view is removed from the view hierarchy (and from the window) when the view controller is not visible on screen. When the callback is called, your MySubclassedTVC is not visible; your MySubSubclassedTVC just called viewWillDisappear. MySubSubclassedTVC is still covering the screen at this point, and MySubclassedTVC's view has not been added back to the view hierarchy yet.


One solution would be to move the callback to viewDidDisappear():

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        callbackClosure?()
    }


This works because MySubclassedTVC is now visible, and MySubclassedTVC's view is back in the visible view hierarchy (and, therefore, back in the responder chain). Of course, using viewDidDisappear() delays the callback until the "go back" animation is complete, which may or may not be ideal in your situation.

Replies

Where do you registerUndo operation ?


May be you want to have a look at this tutorial :

h ttps://www.raywenderlich.com/5229-undomanager-tutorial-how-to-implement-with-swift-value-types

Thanks. I read that page and sgerrard provided some information in the comments that helped. I now have verified that the

undoManager
object is available in the
viewDidAppear()
method, but I think the inheritance hierarchy I created is messing things up. By that, I mean that I subclassed
UITableViewController
into
MyTVC
. I added a
viewDidAppear()
method and the
undoManager
appears in both
self
and
view
. So far so good.


But I have multiple similar tableViews, so I factored out the common code and left that in

MyTVC
and created two new subclasses, which I'll call
MyTVC_A
and
MyTVC_B
. (The Template Pattern, if you're familiar with such.) When I put print statements in
MyTVC.viewDidAppear()
and
MyTVC_A.viewDidAppear()
, they work; the
undoManager
property is non-
nil
in both places and for both
self
and
view
. But...


I have

MyTVC_A.viewWillDisappear()
invoking a closure provided by the parent class as a way to pass data back to it. Trying to access
undoManager
from inside the closure (i.e. the parent class,
MyTVC
) is failing. Yet it worked in
MyTVC.viewDidAppear()
!?


Is this not the right way to handle passing data back to the parent VC?


(I have this narrowed down to a few lines of code in a single file, and this gist contains both the `.swift` and the `.storyboard` (see the comment at the bottom): https://gist.github.com/Azhrei/265ce5c12be9e3303239ebdad3a36de0.)


Thanks for your help!

What do you mean by passing data back to the parent ? You mean to an instance of Parent ?


Was it the Parent instance that created and called the child ?


If so, you could

create a protocol ParentDelegate with a func sendDataBack(data: String)

implement sendDataBack(data: String) in Parent

have a delegate in Child : ParentDelegate?

set the delegate to self from the parent (in prepare for segue if you segue to it or just after creating the child instance)

use the delegate in the child to invoke the sendDataBack() in parent: delegate?sendDataBack(data: someString)


Or, if you can access the instance of the parent, just set the property directly


theParent.thePropertyOfSuper = something

Thank you for replying. 🙂


I used "parent" as a generic term, not meaning self.parent; sorry for any confusion. I had hoped that my fervant use of consistent fonts would've been clear...


I'm quite familiar with the concept of using a protocol to delegate storage of data to an object without knowing that object's concrete type. That's part of the standard "program to the interface, not the implementation" that's been drilled into my head for the last 20 years of Java programming. (When I read about the technique of downcasting in the prepare() method to a concrete class it made me nauseous! Thankfully, there are some folks out there who advocate the correct approach, i.e. using a protocol. 🙂)


I've put two images on my web site and linked to them from the GitHub gist in my previous message.


After looking at the class diagram, notice the inheritance hierarchy for MyTableViewVC. That class doesn't appear on the storyboard, but the two subclasses do. But when the subclasses use a closure in MyTableViewVC that tries to access the undoManager, the property contains nil.


You can also find sample code at the github.com gist I created (see my second message). I even pasted the XML for the storyboard there so that the helpful folks here would be able to create a project and copy just two files (.swift and .storyboard) and recreate the issue.


Perhaps no one else has this problem?? Is that because no one else uses proper OO design techniques, including inheritance? I guess I may file this as a bug with Apple (and provide the link to the sample code) and see what they say. If they say anything. I've never had great luck with getting responses from them. 😟

I’m not a Swift guy but I also use the delegate pattern as Claude suggested. You can still completely decouple the child so it doesn’t have to know about who its parent is; only that some object implements the protocol in which you’ve defined the messages the child might send.


Whether there’s something weird about how Swift captures self in closures, I can’t say. But it might be worth a try using a delegate protocol instead of a closure.

Thanks for your reply. I'm not a Swift guy either (yet). 🙂


I swapped out the closure for a delegate. No luck. In fact, I factored out the code into a separate function and then invoked the function via a closure (no `undoManager`) as well as via a delegate (no `undoManager`). Sigh.


I've updated the gist in my previous message so that it includes the new -- refactored and delegate-ized (?) -- source code, and edited my descriptive comments to show the new output.


I've filed a bug report with Apple; you can find the openradar report here: https://openradar.appspot.com/radar?id=5001734573260800

The reason undoManager is nil is that the next responder is nil—indicating that your view controller is not part of the responder chain. I modified your callbackFunction() method to show this (I added lines 3 and 4 in the following snippet):

    func callbackFunction(_ msg: String) -> Void {
        let typ = "MyTableViewVC"
        print("\(typ).callbackFunction next responder: \(next)")
        print("\(typ).callbackFunction view.superview: \(view.superview)")
        if let _ = self.undoManager {
            print("\(typ).callbackFunction: Found in 'self' \(msg)")
        } else {
            print("\(typ).callbackFunction: NOT found in 'self' \(msg)")
        }
        if let _ = self.view.undoManager {
            print("\(typ).callbackFunction: Found in 'self.view' \(msg)")
        } else {
            print("\(typ).callbackFunction: NOT found in 'self.view' \(msg)")
        }
    }


Output (just from that method):

MyTableViewVC.callbackFunction next responder: nil
MyTableViewVC.callbackFunction view.superview: nil
MyTableViewVC.callbackFunction: NOT found in 'self' via closure
MyTableViewVC.callbackFunction: NOT found in 'self.view' via closure
MyTableViewVC.callbackFunction next responder: nil
MyTableViewVC.callbackFunction view.superview: nil
MyTableViewVC.callbackFunction: NOT found in 'self' via delegate
MyTableViewVC.callbackFunction: NOT found in 'self.view' via delegate


So, why is the next responder nil? From the documentation of UIResponder.next (bold added for emphasis):

The UIResponder class does not store or set the next responder automatically, so this method returns nil by default. Subclasses must override this method and return an appropriate next responder. For example, UIView implements this method and returns the UIViewController object that manages it (if it has one) or its superview (if it doesn’t). UIViewController similarly implements the method and returns its view’s superview. UIWindow returns the application object. The shared UIApplication object normally returns nil, but it returns its app delegate if that object is a subclass of UIResponder and has not already been called to handle the event.

Note from the output above that the view controller's view's superview is nil also. This is because a view controller's view is removed from the view hierarchy (and from the window) when the view controller is not visible on screen. When the callback is called, your MySubclassedTVC is not visible; your MySubSubclassedTVC just called viewWillDisappear. MySubSubclassedTVC is still covering the screen at this point, and MySubclassedTVC's view has not been added back to the view hierarchy yet.


One solution would be to move the callback to viewDidDisappear():

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        callbackClosure?()
    }


This works because MySubclassedTVC is now visible, and MySubclassedTVC's view is back in the visible view hierarchy (and, therefore, back in the responder chain). Of course, using viewDidDisappear() delays the callback until the "go back" animation is complete, which may or may not be ideal in your situation.

"This is because a view controller's view is removed from the view hierarchy (and from the window) when the view controller is not visible on screen."


Ah. I don't think I've ever seen that documented. In fact, the way I read the docs, the views might not be visible but that doesn't change the hierarchy and because view controllers are never "visible", they should remain in the sequence of objects that will be searched, even if the views are removed.


Thank you!


I guess I can go back to my bug report and add a comment that I've found a resolution (after I test it)! I had already provided some feedback to the documentation team that the text for UIResponder.undoManager could be clearer. So far I've found that the iOS documentation is sometimes too terse to be clear about the functionality...

> Ah. I don't think I've ever seen that documented.


This is documented (albeit unclearly) in UIViewController "Handling View-Related Notifications" and UIViewController.viewWillDisappear:

When the visibility of its views changes, a view controller automatically calls its own methods so that subclasses can respond to the change. Use a method like viewWillAppear(_:) to prepare your views to appear onscreen, and use the viewWillDisappear(_:) to save changes or other state information. Use other methods to make appropriate changes.
viewWillDisappear(_:)
This method is called in response to a view being removed from a view hierarchy. This method is called before the view is actually removed and before any animations are configured.

> In fact, the way I read the docs, the views might not be visible but that doesn't change the hierarchy and because view controllers are never "visible", they should remain in the sequence of objects that will be searched, even if the views are removed.


The view controller hierarchy does remain intact. In fact, if you call UIViewController.parent on your MySubclassedTVC, you'll get the UINavigationController. The confusion results in the fact that the responder chain contains entities from both the view hierarchy and the view controller hierarchy. I'm not sure why they couldn't just implement UIViewController's next responder to be its parent view controller's view (which would preserve the responder chain even if the view hierarchy is broken).


> So far I've found that the iOS documentation is sometimes too terse to be clear about the functionality...


Agreed. The documentation reshuffling that occurred a few months ago has made things even worse than they already were.

Thanks for clarifying. From the documentation that you quoted, I don't see any sign that view controllers are removed from the view hierarchy (although I haven't followed the links and read the entire document), although views are removed as stated in viewWillDisappear, as you described. And both are documented to be in the responder chain... But all of that is moot at this point. 🙂


It appears that I have two choices. The view that is active should be able to access the self.undoManager property and because of the dynamic lookup within the hierarchy, it should retrieve the undoManager created by the nav controller. I could therefore access the undoManager and pass it as a parameter to the callback function. [Edit: Nope, this won't work. I don't know what I was thinking when I wrote this earlier, but if non-visible views aren't in the hierarchy, they can't be found dynamically.]


Or, I could move the invocation of the callback to the viewDidDisappear, as you suggested, since the view is gone and the layer below would be added back into the hierarchy, and thus, would have an undoManager property. But this means that there's no way to create an undoManager property in the nav controller that applies to all views that the nav controller might display. Unless I create my own property that holds my own UndoManager instance that is inherited by and can be retrieved by any subclass. [This seems like a design flaw in UIKit. I can't think of any reason why it must work like this to support their environment, and a lot of reasons, both from a practical and a design/architecture perspective, why view controllers should stay in the hierarchy and lookup of the undoManager should search those view controllers. Unless it has something to do with memory management — views not in the hierarchy could be purged from memory and reloaded later when added back?]


Thanks again. I really appreciate your in-depth knowledge about the view hierarchy!


(And I have left my bug report open and referenced this discussion in regards to documentation cleanup.)

The solution I ended up with is to walk the

.parent
property chain until I find a non-nil
undoManager
property.


Doing this results in finding an

undoManager
in the nav controller without my having to create one myself. For my purposes, this is great. If I had wanted to go all the way up the chain to the tab bar, I'd need to determine the how/why the nav controller was creating one on its own, or just walk the chain until
let _ = node.parent as? UITabBarController
returned
true
.