Uncategorized

Debugging _addChildViewController:performHierarchyCheck:notifyWillMove:

Introduction

I’ve recently been working on a UISplitViewController-esque view controller based around the requirements of my app (more on what’s wrong with UISplitViewController in a later post).  Because we support Adaptive Layout, I’m adding a behavior where when the view controller gets too narrow, it merges the master and detail navigation controllers into master, and when it gets wide again, it splits them back out again.

This works fine in during size transitions (rotating the device or using iPad split view) but can fail with the following exception when state restoration crosses the size boundary:

child view controller:<SEQuestionAndAnswerViewController: 0x7ff4e780e000> should have parent view controller:<SEUINavigationController: 0x7ff4e5035400> but requested parent is:<SEUINavigationController: 0x7ff4e7009000>

Or for clarity:

child view controller:              THING I'M MOVING
should have parent view controller: SOURCE NAVIGATION CONTROLLER
but requested parent is:            TARGET NAVIGATION CONTROLLER

So, what’s going on here? If I check the child view controller’s parent, it’s nil, so something weird is happening.  Why does UIKit think the source navigation controller owns the child?

Figuring out what’s wrong

To answer this question, I like to turn to my trusty source for undocumented behavior, Hopper Disassembler.  Hopper is a tool for inspecting executables and libraries, which can take machine code and translate it into somewhat readable Objective-C.

First I find the throwing method in the call stack,  -[UIViewController _addChildViewController:performHierarchyCheck:notifyWillMove:]:

Screen Shot 2017-06-29 at 3.47.46 PM.png
The machine code above the crash

It looks truly terrible in Xcode but does give us some hints, selectors like window, parentViewController, _existingView, superview, and _viewControllerForAncestors are used before the exception.  I could start trying probing the value of each of those before continuing, but let’s see that same method in Hopper:

Screen Shot 2017-06-29 at 3.31.15 PM.png
The same code, disassembled

That’s more or less readable.  The hierarchy check is as follows:

  • If the view controller has a parent view controller, it checks out. I don’t really understand this, but I’ll go with it.
  • Otherwise, if the view controller hasn’t loaded its view, or the view isn’t in a window, we’re good.
  • If neither of those check out, we get call [child.view.superview _viewControllerForAncestor] to get the superview’s view controller.  If this view controller is not nil and not the new parent, then we crash.

This is pretty easy to check in the debugger.  Calling the method gets us what we want.  Unfortunately, we don’t have _viewControllerForAncestor available to us, but we can fake it by calling nextResponder over and over until we get a view controller:

Screen Shot 2017-06-29 at 4.00.59 PM.png
Eventually we get there.

This is good, because now we can check if we’re going to crash before we actually do.  But what can we do about it?

Putting together a solution

There are two solutions.  One would be to call viewController.view.removeFromSuperview() before moving it, but who knows what that will break inside UINavigationController.  The other is to have faith that eventually UINavigationController will let go of the view and then we can finish what we’re doing.

I went with the later and it looks like they’re let go in the next run of the main loop, so DispatchQueue.main.async is all I need before I’m ready to update my view.  The only problem is that one frame renders with my view controller missing.  I’m just putting a blank navigation controller there so it looks like the content is just loading (which, in a sense, it is).

My final code looks something like this:

Epilogue

Looping back to one thing I I glossed over.  Why is this happening and why doesn’t it happen during size changes?  I haven’t fully looked into it, but if you look at -[UINavigationController setViewController:animated:] (which is not nearly as cleanly disassembled), you’ll see that transitions can be deferred under certain circumstances, and needsDeferredTransition is true, so that’s probably what happened.

It doesn’t (can’t?) do it during size changes because -[UIViewController viewWillTransitionToSize:withTransitionCoordinator:] is called within +[UIViewController _performWithoutDeferringTransitions:]. Why state restoration isn’t is anyone’s guess.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s