UIViewController's view loading process demystified

@NatashaTheRobot posted about testing view controllers and issues with loading views on her blog in The One Weird Trick For Testing View Controllers in Swift post.

She also wrote:

The key here is that Apple overrides the viewController’s view getter to call the loadView function and do a bunch of other things we have no access to. If anyone else has other great insights into why this works, feel free to add it in the comments!

Yeah.. this is interesting what is going on under the hood. She inspired me and I started digging and debugging a bit. I’ve noticed there are two flows - one when view controller is assigned to window as root view controller and one when it is not (e.g. when you want to test view controller and you instantiated it from storyboard).

View Controller as a rootViewController

if self.window == nil {
    self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
}

let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc = storyboard.instantiateViewControllerWithIdentifier("ViewController")
self.window!.rootViewController = vc

This is the flow which takes a place e.g. in application:DidFinishLaunchingWithOptions: method.

image-1

The flow starts by calling makeKeyAndVisible method of UIWindow which calls its private addRootViewControllerViewIfPossible method that tries to add view of root view controller to itself and presents it. UIWindow accesses view property of the root view controller which starts a chain of loading view process. The accessor calls loadViewIfRequired method which calls loadView method. The loadView calls internal methods of UIViewController that loads nib with a view that will be set.

There is one great talk from this year’s WWDC which covered how Storyboard and their nibs behaves at runtime - Implementing UI Design in Interface Builder.

After the loaded view is set, view controller calls its internal _window method and read all the things like preferedInterfaceOrientation, supportedInterfaceOrientations, shouldAutorotate, etc. Actually it calls _window many times and there are some other methods called too.

Next viewDidLoad method is called and private __viewWillAppear which calls viewWillAppear on view controller. View is about to be presented so there are calls to willMoveToWindow:, willMoveToSuperview: and private _didMoveFromWindow:toWindow: methods.

The next thing is setting up Auto Layout in the view, so layoutMarginsDidChange, didMoveToWindow, didMoveToSuperview, updateViewConstraints, updateConstraints, layoutSublayersOfLayer, viewWillLayoutSubviews, layoutSubviews methods are called - there is a lot of them.

Finally viewDidAppear: is called and view is presented.

View Controller loaded in testing

This is another important case worth to investigate. In this case you probably don’t want to create window and put your view controller under the test to it. You just want to instantiate view controller instance from storyboard and test it.

Before Natasha’s post I known just one method to load view of a view controller and make sure it is loaded. I did it by accessing view property directly after view controller was created. Today I’ve learned about loadViewIfNeeded method which is available since iOS 9 and behaves the same way like accessing view property. I’ve also learned third - IMO the best method to load the view controller during testing and make sure it is ready to use - presented by Ørta. This is about calling beginAppearanceTransition:animated and endApperanceTransition methods. Here is solution he shared - covered below.

Accessing view directly

Let’s take a look on a flow when accessing view directly.

let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc = storyboard.instantiateViewControllerWithIdentifier("ViewController")
_ = vc.view

And the flow is like below:

image-2

Okaaay… There is much less calls. View has been accessed and loaded. The window is nil so I think no more methods have been called and the flow ended up in viewDidLoad. A bit strange. Some configuration might be inside viewWillAppear or viewDidLoad. This method should work in most test cases. loadViewIfNeeded works the same way.

Let’s check how beginApperanceTransition:animated: behaves.

beginApperanceTransition:animated

let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc = storyboard.instantiateViewControllerWithIdentifier("ViewController")
vc.beginAppearanceTransition(true, animated: false)
vc.endAppearanceTransition()

image-3

Great! This flow is more similar to the first one. You can see there is no window here so the view of view controller is not added to the flow so no auto layout configuration code is running. Just view controller configuration.

Conclusion

You can see the loading view flow depends of in which context view controller and view are used.

In the first case there was a view and view controller was the root one so it has a lot of configuration and view was configured too.

In the last case there is just view controller configuration since there is no window and configuring view is not necessary. This approach also simulated presenting view controller so viewWillAppear and viewDidAppear method have been called - This might be important in some cases.

And the last case I would mention is the second one. I didn’t know earlier that it behaves like this. I would expect that viewWill/viewDid method are called too, but they didn’t. IMO the approach presented by Ørta is the best one when testing view controllers.

--
2015-2018 All rights reserved. Tomasz Szulc