iosdev

Importance of isViewLoaded when embedding child controllers

UIViewController containment APIs are very old by now. They can still cause headache if you don’t pay attention to view loading process.

As iOS devices become larger in all dimensions, importance of mastering creation of custom container controllers increases. When you can fit more content on the screen you inevitably will. Which naturally complicates the technical implementation of such screens: more data means more data sources which leads to more code which…[cue the M(assive)VC cries…]

In order to KISS1, I always recommend to compose complex UI using as many child UIViewControllers as needed. Each UIVC should ideally have one data source and one complex view.

API to create your own container controllers is very old; look for Managing Child View Controllers in a Custom Container in UIViewController reference docs. It’s dating back to iOS 5 thus by now it works reliably well.

There’s one bit here that I feel is not given enough spotlight: isViewLoaded property.

The value of this property is true when the view is in memory or false when it is not. Accessing this property does not attempt to load the view if it is not currently in memory.

I have highlighted the most important piece. Let me explain why.

Containment basics

The first job of the container controller is to keep a reference to its children. Second job is to control its own layout, the containing views where embedding happens. Imagine custom-designed container for any kind of content UIVC:

final class CustomContainerController: UIViewController {
	@IBOutlet private var containingView: UIView!
	private(set) var embeddedController: UIViewController?
}

Embedding controllers

Let’s review the typical process that container controller will do when embedding a child view controller:

// (1)
addChild(vc)
containingView.addSubview(vc.view)
// setup layout between `vc.view` and its `containingView`
vc.didMove(toParent: self)

// (2)
embeddedController = vc

The first part can be wrapped into an embed method:

embed(controller: vc, into: containingView) {
	...
}

and then combined with second part it’s a simple two-line public method:

final class CustomContainerController: UIViewController {
	func display(vc: UIViewController) {
		embed(controller: vc, into: containingView)
		embeddedController = vc
	}
}

This works just fine, as long as you call display(vc:) method after CustomContainerController is already loaded and visible on the screen; meaning – it’s already part of the app’s view hierarchy.

The right thing will happen and embedded ContentController’s view will be added as child of the containingView, as Xcode’s view debugger confirms:

The problem appears when you do something like this:

let vc = ContentController.instantiate()

let cc = CustomContainerController.initial()
cc.display(vc: vc)

present(cc, animated: true)

Here I instantiate both VCs and inject the content into container before the container’s view was added to overall view hierarchy. This results in premature view loading at this line of the embed process:

containingView.addSubview(vc.view)

and in the very next line:

// setup layout between 
// `vc.view` and its `containingView`

whatever layout code you added may fail to work properly, throw weird exceptions or (even worse) work in unexpected ways because CustomContainerController’s view is not laid out at that point. It’s free-standing in memory, not yet added to app’s UIWindow instance.

It gets added only when you call present(vc:animated:completion:) method. That will most likely trigger another layout pass, causing strange glitches, especially if you use custom popup animations.

The solution is to prevent premature view loading and let UIKit do its thing in due time. Here’s how you can do that:

final class CustomContainerController: UIViewController {
	func display(vc: UIViewController) {
		embeddedController = vc
		if !isViewLoaded { return }
		embedIfNeeded()
	}
	
	override func viewDidLoad() {
		super.viewDidLoad()
		embedIfNeeded()
	}
	
	private func embedIfNeeded() {
		guard let vc = embeddedController else { return }
		embed(controller: vc, into: containingView)
	}
}

Thus final process for embedding child UIVC is this:

  1. Save the reference to VC being embedded.
  2. Check for isViewLoaded and if false, bail out.
  3. Otherwise proceed to perform actual embed.
  4. In viewDidLoad, perform the actual embed.

Points 3 and 4 are mutually exclusive thus you will not embed the same view twice.

Unembed

When removing child controller, process is similar and mostly inverse:

// (1)
vc.willMove(toParent: nil)
if vc.isViewLoaded {
	vc.view.removeFromSuperview()
}
vc.removeFromParent()

// (2)
embeddedController = nil

isViewLoaded plays an important part here as well. In very complex UIs, you can have some controllers which are preloaded but their view may be unloaded due to memory constraints or maybe it was waiting for the right conditions. Thus the check for vc.isViewLoaded prevents unnecessary view load only to have it removed immediately.

Essentials

This embed / unembed dance is mostly boilerplate; hence it’s good idea to extract the boring, repeating lines into a method. That way, you’ll never mis-cue the calls or forget one of the required calls.

Look for Controllers/Embeddable.swift in my Essentials library for two useful method extensions on UIViewController.

Embed method has a trailing closure where you can supply an auto-layout custom code. By default, it will just align the edges of the embedded view with the edges of its container.

This extension leaves you free to focus on the right way to call embed() and when to check for isViewLoaded. As a rule of thumb — any code that looks like willSet/didSet property observer in UIViewController needs to check for isViewLoaded.


  1. Keep It Simple (Stupid) ↩︎