iOS
ViewController
Swift
UIKit
CodeIntegration

Adding a view controller as a subview in another view controller

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If you want one view controller to appear inside another, the correct UIKit pattern is view-controller containment, not just adding the child controller's view as a random subview. Proper containment keeps lifecycle callbacks, rotation behavior, responder-chain behavior, and memory management consistent.

Why a Plain Subview Is Not Enough

A UIViewController manages more than a view. It also participates in appearance callbacks, child relationships, transitions, and system behaviors.

If you only do this:

swift
parent.view.addSubview(child.view)

the UI may appear on screen, but UIKit has not been told that the parent-child controller relationship exists. That leads to subtle bugs later.

The Correct Containment Sequence

The standard sequence is:

  • call addChild
  • add the child controller's view to the hierarchy
  • size or constrain the view
  • call didMove(toParent:)

A minimal example looks like this.

swift
1import UIKit
2
3final class ParentViewController: UIViewController {
4    private let child = ChildViewController()
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8
9        addChild(child)
10        view.addSubview(child.view)
11        child.view.translatesAutoresizingMaskIntoConstraints = false
12
13        NSLayoutConstraint.activate([
14            child.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
15            child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
16            child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
17            child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
18        ])
19
20        child.didMove(toParent: self)
21    }
22}
23
24final class ChildViewController: UIViewController {
25    override func viewDidLoad() {
26        super.viewDidLoad()
27        view.backgroundColor = .systemBlue
28    }
29}

This is the supported containment pattern in UIKit.

Why addChild and didMove Matter

addChild tells UIKit that the parent is taking ownership of the child controller relationship. didMove(toParent:) tells the child that the move is complete.

Without those calls, the child may miss appearance propagation and other lifecycle behavior.

The sequence also matters. Call addChild before inserting the view, and call didMove(toParent:) after the view has been added and constrained.

Removing a Child Controller Cleanly

Containment has a matching removal sequence too.

swift
1func removeChildController(_ child: UIViewController) {
2    child.willMove(toParent: nil)
3    child.view.removeFromSuperview()
4    child.removeFromParent()
5}

Skipping these steps can leave the controller hierarchy inconsistent and make debugging memory-retention issues more difficult.

Use a Container View When Appropriate

In storyboard-based apps, a container view is often the cleanest way to embed a child controller. Under the hood, UIKit still uses the same containment model. The advantage is that the visual relationship is declared in Interface Builder.

In fully programmatic UIs, manual containment gives more control and is often clearer when the embedded child is dynamic.

Common Use Cases

View-controller containment is useful for:

  • reusable dashboard panels
  • custom tab or paging containers
  • embedding a list or map module inside a larger screen
  • replacing parts of a screen without presenting a whole new scene

The key benefit is separation of concerns. The child controller owns its own view logic while the parent controls placement and coordination.

Common Pitfalls

The biggest mistake is adding only the child view and forgetting addChild and didMove(toParent:).

Another mistake is constraining the child view incorrectly or not at all, which makes it look like containment failed when the real problem is layout.

A third issue is forgetting the mirrored removal sequence when replacing embedded controllers.

Finally, do not overuse containment for trivial reusable view pieces. If a component has no controller behavior and only needs layout, a plain UIView may be a better abstraction.

Summary

  • Use view-controller containment when embedding one controller inside another.
  • The correct sequence is addChild, add the view, apply layout, then call didMove(toParent:).
  • Removing a child also has a required sequence with willMove(toParent: nil) and removeFromParent().
  • Proper containment preserves lifecycle behavior and avoids subtle UIKit bugs.
  • Use container views or manual containment depending on how dynamic the UI is.
  • If you only need reusable visuals, consider a custom UIView instead of another controller.

Course illustration
Course illustration

All Rights Reserved.