The UIViewController: Actual Lifecycle and Acceptable Heirachy
I am working on an iOS app for a product that I have been building. Throughout the process I have come up against some hurdles and have sought to resolve them using the (fantastic) knowledge base that is Stack Overflow.
Moreso than when writing code for any other platform I have found that Stack Overflow answers pertaining to Objective C/Swift are full of inaccuracies, are misleading, or are down right wrong. As such I have spent a lot more time investigating issues myself and working out exactly why things happen and how things work.
Apple has extremely good documentation of its APIs, and application guidelines. What confuses me somewhat is the fact that they have not taken the time to write in depth expanations of areas that might not be so obvious and areas that are often discussed and debated.
Given that a lot of the internals of Apple's APIs are private and one cannot simply look for an answer I think this is something they should invest some time in.
Recently I have encountered a number of considerations relating to the heirachy and lifecycle of UIViewControllers.
Issue - UITabBarController in the heirachy
Why exactly does your UITabBarController have to be the root controller? If you read the UITabBarController API documentation it clearly states When deploying a tab bar interface, you must install this view as the root of your window. Why is this?
Using XCode 6 and iOS 8 I embedded a UITabBarController as a child at numerous levels of the heirachy without issue. I am aware that in previous versions you could not.. but as things stand, you can. It would thus seem that at the moment the only reason not to do this is because Apple says not to.
Hands on
In the app that I am building I wanted to have tab bar navigation at the base of the application. In each tab various controls would allow you to open other views which I also wanted to contain independent tabbed navigation. This is not allowed (as outlined above).
After digging a bit I found out that actually it is.. The documentation states that It is possible (although uncommon) to present a tab bar controller modally in your app.
As the tab bar controller always acts as the wrapper for the navigation controllers each tab has to have its view controller embedded in a UINavigationController. Given that I want all the tabs to have the same navigation controls.. this is just annoying. Especially given that the docs state that you should embed only instances of the UINavigationController class, and not system view controllers that are subclasses of the UINavigationController class.
It is extremely unclear as to whether you are 'allowed' to use your own custom UINavigationController subclasses. My interpretation is that it is OK. If you are only doing small manipulations and are calling the respective super
methods I can not see any reason why this would be an issue.
Issue - Why is viewWillAppear not consistently called?
What exactly is the UIViewController Lifecycle, and why does it vary under certain nesting circumstances?
For example viewWillAppear
is not consistently called in a UIViewController nested in a UINavigationController displayed in a modal..
There is an example of a similar issue here. I dont personally reccomend you use this answer. What I do recommend is that if you have complex or 'uncommon' view heirachies you verify that the lifecycle methods you expect to be called are in fact called.
Issue - manipulating views based on resolved constraints.
Another intriguing issue is manipulating views when their sub views have been laid out. Apple does have the viewDidLayoutSubviews
method, but again it is unclear exactly when this method is called. The documentation states this method being called does not indicate that the individual layouts of the view'€™s subviews have been adjusted. Each subview is responsible for adjusting its own layout. - this can lead to some interesting considerations which i have outlined below.
Hands on
In my modally presented UITabBarController I have a UIViewController (nested in a UINavigationController) in which i want to lay out some buttons based on the space available to me when my constraints have been resolved. To make things a little more complex, this is within a UIScrollView.
When my viewDidAppear
method is called, my constraints have been resolved. Unfortunately however positioning and adding subviews here will at a minimum cause some flickering as they are displayed. This is not acceptable.
viewDidLayoutSubviews
is called at undocumented times. I found from testing that the viewDidLayoutSubviews
was in fact called twice, and that after the first call the subviews of my UIScrollView were in fact not layed out. Only after the second execution were all my constraints resolved.
I have no interest in doing any complex error prone conditionals such as calculate and add subviews the second time viewDidLayoutSubviews
is called. As such I decided the most definitive way of knowing when my scroll views subviews had been layed out was by creating a custom subclass of UIScrollView and overriding its layoutSubviews
method.
The actual view controller lifecycle for my setup is listed below. The frame size is also noted.
- viewWillAppear (0.0,0.0,320.0,568.0)
- The layoutSubviews method of my base view(0.0,0.0,320.0,568.0)
- viewDidLayoutSubviews (0.0,0.0,320.0,568.0)
- the layoutSubviews method of my scroll view (20.0,426.0,280.0,200.0) correct resolved frame
- The layoutSubviews method of my base view (20.0,426.0,280.0,200.0) again
- viewDidLayoutSubviews (20.0,426.0,280.0,200.0)
- viewDidAppear (20.0,426.0,280.0,200.0)
The important thing to note here is that you cannot just assume that because viewDidLayoutSubviews
has been called that all your constraints have been resolved. The name is totally misleading, but its a private API and there is nothing we can do about it sadly.
Because the layoutSubviews
method can also be called numerous times it is important to make sure you dont run complex process operations more often than necessary. In my case within layoutSubviews
I have a simple check which verifies if my frame has changed since it was last processed. If it hasn't there is no need to re-process anything.
All things considered
After going to the effort to work out the above it hit me that my codebase was now significantly cleaner. I had seperated my concerns to a greater extent and it felt more MVC esque.
My manipulation of my views is now in a subclass of UIScrollView rather than in my UIViewController - my controller is now more targetted towards control and my view focussed on.. well.. the view.
I read somewhere in the Apple documentation that view manipulation from a UIViewController is perfectly acceptable. It is in the name really :) That said I find it incredibly intriguing that the way Apple has build its product and presented it to developers inherently results in what I believe to be better designed code bases.
I have learned a lot because Apple's codebase is private. Some more documentation would still be appreciated :)