144

A Container View can be easily added into a storyboard through Interface Editor. When added, a Container View is of a placeholder view, an embed segue, and a (child) view controller.

However, I am not able to find a way to add a Container View programmatically. Actually, I am not even able to find a class named UIContainerView or so.

A name for the class of Container View is surely a good start. A complete guide including the segue will be much appreciated.

I am aware of View Controller Programming Guide, but I do not regard it as the same as the way Interface Builder does for Container Viewer. For example, when the constraints are properly set, the (child) view will adapts to the size changes in Container View.

Karthik Kumar
  • 1,375
  • 1
  • 12
  • 29
Dante May Code
  • 11,177
  • 9
  • 49
  • 81
  • 1
    What do you mean when you say "when the constraints are properly set, the (child) view will adapts to the size changes in Container View" (thereby implying that this is not true when you do view controller containment)? Constraints work the same whether you did it via container view in IB or view controller containment programmatically. – Rob May 22 '16 at 20:41
  • 1
    Most important thing is the embedded `ViewController`'s life cycle. The embedded `ViewController`'s life cycle by Interface Builder is normal, but the one added programmatically has `viewDidAppear`, neither `viewWillAppear(_:)` nor `viewWillDisappear`. – DawnSong Aug 18 '17 at 06:37
  • 2
    @DawnSong - If you do the view containment calls correctly, the `viewWillAppear` and `viewWillDisappear` are called on the child view controller, just fine. If you have an example where they're not, you should clarify, or post your own question asking why they're not. – Rob Aug 18 '17 at 14:09

4 Answers4

277

A storyboard "container view" is just a standard UIView object. There is no special "container view" type. In fact, if you look at the view hierarchy, you can see that the "container view" is a standard UIView:

container view

To achieve this programmatically, you employ "view controller containment":

  • Instantiate the child view controller by calling instantiateViewController(withIdentifier:) on the storyboard object.
  • Call addChild in your parent view controller.
  • Add the view controller's view to your view hierarchy with addSubview (and also set the frame or constraints as appropriate).
  • Call the didMove(toParent:) method on the child view controller, passing the reference to the parent view controller.

See Implementing a Container View Controller in the View Controller Programming Guide and the "Implementing a Container View Controller" section of the UIViewController Class Reference.


For example, in Swift 4.2 it might look like:

override func viewDidLoad() {
    super.viewDidLoad()

    let controller = storyboard!.instantiateViewController(withIdentifier: "Second")
    addChild(controller)
    controller.view.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(controller.view)

    NSLayoutConstraint.activate([
        controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
        controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
        controller.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
        controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10)
    ])

    controller.didMove(toParent: self)
}

Note, the above doesn't actually add a "container view" to the hierarchy. If you want to do that, you'd do something like:

override func viewDidLoad() {
    super.viewDidLoad()

    // add container

    let containerView = UIView()
    containerView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(containerView)
    NSLayoutConstraint.activate([
        containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
        containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
        containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
        containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
    ])

    // add child view controller view to container

    let controller = storyboard!.instantiateViewController(withIdentifier: "Second")
    addChild(controller)
    controller.view.translatesAutoresizingMaskIntoConstraints = false
    containerView.addSubview(controller.view)

    NSLayoutConstraint.activate([
        controller.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
        controller.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        controller.view.topAnchor.constraint(equalTo: containerView.topAnchor),
        controller.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
    ])

    controller.didMove(toParent: self)
}

This latter pattern is extremely useful if ever transitioning between different child view controllers and you just want to make sure one child's view is in the same location and the previous child's view (i.e. all the unique constraints for the placement are dictated by the container view, rather than needing to rebuild these constraints each time). But if just performing simple view containment, the need for this separate container view is less compelling.


In the examples above, I’m setting translatesAutosizingMaskIntoConstraints to false defining the constraints myself. You obviously can leave translatesAutosizingMaskIntoConstraints as true and set both the frame and the autosizingMask for the views you add, if you’d prefer.


See previous revisions of this answer for Swift 3 and Swift 2 renditions.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    I don't think your answer is complete. Most important thing is the embedded `ViewController`'s life cycle. The embedded `ViewController`'s life cycle by Interface Builder is normal, but the one added programmatically has `viewDidAppear`, neither `viewWillAppear(_:)` nor `viewWillDisappear`. – DawnSong Aug 18 '17 at 06:40
  • Another strange thing is that embedded `ViewController`'s `viewDidAppear` is called in its parent's `viewDidLoad`, instead of during its parent's `viewDidAppear` – DawnSong Aug 18 '17 at 06:46
  • @DawnSong - "but the one added programmatically has `viewDidAppear`, [but] neither `viewWillAppear(_:)` nor `viewWillDisappear`". The `will` appear methods are called correctly in both scenarios. One must call `didMove(toParentViewController:_)` when doing it programmatically, tho, or else they won't. Regarding the timing of the appearance. methods, they are called in the same sequence both ways. What does differ, tho, is the timing of `viewDidLoad`, because with embed, it's loaded before `parent.viewDidLoad`, but with programmatic, as we'd expect, it happens during `parent.viewLoadLoad`. – Rob Aug 18 '17 at 08:48
  • 2
    I was stuck on constraints not working; turns out I was missing `translatesAutoresizingMaskIntoConstraints = false`. I don't know why it's needed or why it makes things work, but thank you for including it in your answer. – hasen Feb 15 '18 at 07:25
  • 1
    @Rob At https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html in Listing 5-1, there is a line of Objective-C code that says, "content.view.frame = [self frameForContentController];". What is "frameForContentController" in that code? Is that the frame of the container view? – daniel Feb 21 '19 at 03:31
  • @DanielBrower - That’s nothing special, equivalent to saying “assume you have some some method that calculates the appropriate `frame`”. Bottom line, set the frame or constraints however appropriate for your app. – Rob Feb 21 '19 at 04:04
  • @hasen See documentation for `translatesAutoresizingMaskIntoConstraints` [here](https://developer.apple.com/documentation/appkit/nsview/1526961-translatesautoresizingmaskintoco) for `NSView` and [here](https://developer.apple.com/documentation/uikit/uiview/1622572-translatesautoresizingmaskintoco) for `UIView`. My summary is that setting this to true on a view makes the containing view act as if the frame's initial size position (within the superview) are set via constraints, so that the subView becomes an "island" whose size and position are immune from auto-layout happening around it. – brotskydotcom Feb 24 '19 at 16:49
  • @Rob your (fabulous, thank you) answer uses constraints on the subView, whereas Apple's [guide](https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html) uses a fixed frame size and position. The use of `translatesAutoresizingMaskIntoContraints` is key to that difference. For those whose prior experience is just with Storyboard segues, it might be helpful if you talked about this explicitly in your answer. – brotskydotcom Feb 24 '19 at 16:54
  • @ancientHacker - It’s a matter of personal preference as to whether one sets the `frame` and `autoresizingMask` while leaving `translatesAutoresizingMaskIntoContraints` as `true`, or setting it to `false` and setting the constraints. (As an aside, this decision has nothing to do with storyboard segues. Whenever you programmatically create a view, you decide whether you want to set the constraints, or set `frame`/`autoresizingMask` and let the OS translate those into constraints for you.) Frankly, it seems a little beyond the scope of the question, but I added a clarification to the answer, – Rob Feb 24 '19 at 17:28
  • 1
    Thank you! Been fussing with trying to do this (with an external storyboard) in XCode for days but the UI did not seem to give me a way to set the segue right. In my case, doing it programmatically was just fine and your solution worked great. – Jc Nolan Jan 05 '21 at 21:30
  • 1
    translatesAutoresizingMaskIntoConstraints = false is an important thing to remember while adding views programmatically. – Manish Singh Jan 07 '21 at 18:52
  • Thanks, with this approach, I have implemented the same UI with Apple music miniPlayer on top of tabBar. – Zhou Haibo Jan 24 '21 at 11:15
  • https://www.swiftbysundell.com/basics/child-view-controllers/ and https://www.swiftbysundell.com/articles/using-child-view-controllers-as-plugins-in-swift/ also have a good writeup on this. – CyberMew Sep 23 '21 at 08:22
29

@Rob's answer in Swift 3:

    // add container

    let containerView = UIView()
    containerView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(containerView)
    NSLayoutConstraint.activate([
        containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
        containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
        containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
        containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
        ])

    // add child view controller view to container

    let controller = storyboard!.instantiateViewController(withIdentifier: "Second")
    addChildViewController(controller)
    controller.view.translatesAutoresizingMaskIntoConstraints = false
    containerView.addSubview(controller.view)

    NSLayoutConstraint.activate([
        controller.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
        controller.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        controller.view.topAnchor.constraint(equalTo: containerView.topAnchor),
        controller.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])

    controller.didMove(toParentViewController: self)
Bright
  • 5,699
  • 2
  • 50
  • 72
19

Here is my code in swift 5.

class ViewEmbedder {

class func embed(
    parent:UIViewController,
    container:UIView,
    child:UIViewController,
    previous:UIViewController?){

    if let previous = previous {
        removeFromParent(vc: previous)
    }
    child.willMove(toParent: parent)
    parent.addChild(child)
    container.addSubview(child.view)
    child.didMove(toParent: parent)
    let w = container.frame.size.width;
    let h = container.frame.size.height;
    child.view.frame = CGRect(x: 0, y: 0, width: w, height: h)
}

class func removeFromParent(vc:UIViewController){
    vc.willMove(toParent: nil)
    vc.view.removeFromSuperview()
    vc.removeFromParent()
}

class func embed(withIdentifier id:String, parent:UIViewController, container:UIView, completion:((UIViewController)->Void)? = nil){
    let vc = parent.storyboard!.instantiateViewController(withIdentifier: id)
    embed(
        parent: parent,
        container: container,
        child: vc,
        previous: parent.children.first
    )
    completion?(vc)
}

}

Usage

@IBOutlet weak var container:UIView!

ViewEmbedder.embed(
    withIdentifier: "MyVC", // Storyboard ID
    parent: self,
    container: self.container){ vc in
    // do things when embed complete
}

Use the other embed function with non-storyboard view controller.

DeyaEldeen
  • 10,847
  • 10
  • 42
  • 75
Jeffrey Chen
  • 1,777
  • 1
  • 18
  • 29
  • 2
    Great class, however I find myself needed to embed 2 viewControllers within the same master view controller, which your `removeFromParent` call prevents, how would you amend your class to permit this? – GarySabo Jan 24 '18 at 01:18
  • brilliant :) Thank you – Rebeloper Mar 12 '18 at 13:55
  • It is nice example, but how can I add some transition animations to this (embeding, replacing of child view controllers)? – Michał Ziobro Apr 23 '18 at 10:39
13

Details

  • Xcode 10.2 (10E125), Swift 5

Solution

import UIKit

class WeakObject {
    weak var object: AnyObject?
    init(object: AnyObject) { self.object = object}
}

class EmbedController {

    private weak var rootViewController: UIViewController?
    private var controllers = [WeakObject]()
    init (rootViewController: UIViewController) { self.rootViewController = rootViewController }

    func append(viewController: UIViewController) {
        guard let rootViewController = rootViewController else { return }
        controllers.append(WeakObject(object: viewController))
        rootViewController.addChild(viewController)
        rootViewController.view.addSubview(viewController.view)
    }

    deinit {
        if rootViewController == nil || controllers.isEmpty { return }
        for controller in controllers {
            if let controller = controller.object {
                controller.view.removeFromSuperview()
                controller.removeFromParent()
            }
        }
        controllers.removeAll()
    }
}

Usage

class SampleViewController: UIViewController {
    private var embedController: EmbedController?

    override func viewDidLoad() {
        super.viewDidLoad()
        embedController = EmbedController(rootViewController: self)

        let newViewController = ViewControllerWithButton()
        newViewController.view.frame = CGRect(origin: CGPoint(x: 50, y: 150), size: CGSize(width: 200, height: 80))
        newViewController.view.backgroundColor = .lightGray
        embedController?.append(viewController: newViewController)
    }
}

Full sample

ViewController

import UIKit

class ViewController: UIViewController {

    private var embedController: EmbedController?
    private var button: UIButton?
    private let addEmbedButtonTitle = "Add embed"

    override func viewDidLoad() {
        super.viewDidLoad()

        button = UIButton(frame: CGRect(x: 50, y: 50, width: 150, height: 20))
        button?.setTitle(addEmbedButtonTitle, for: .normal)
        button?.setTitleColor(.black, for: .normal)
        button?.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        view.addSubview(button!)

        print("viewDidLoad")
        printChildViewControllesInfo()
    }

    func addChildViewControllers() {

        var newViewController = ViewControllerWithButton()
        newViewController.view.frame = CGRect(origin: CGPoint(x: 50, y: 150), size: CGSize(width: 200, height: 80))
        newViewController.view.backgroundColor = .lightGray
        embedController?.append(viewController: newViewController)

        newViewController = ViewControllerWithButton()
        newViewController.view.frame = CGRect(origin: CGPoint(x: 50, y: 250), size: CGSize(width: 200, height: 80))
        newViewController.view.backgroundColor = .blue
        embedController?.append(viewController: newViewController)

        print("\nChildViewControllers added")
        printChildViewControllesInfo()
    }

    @objc func buttonTapped() {

        if embedController == nil {
            embedController = EmbedController(rootViewController: self)
            button?.setTitle("Remove embed", for: .normal)
            addChildViewControllers()
        } else {
            embedController = nil
            print("\nChildViewControllers removed")
            printChildViewControllesInfo()
            button?.setTitle(addEmbedButtonTitle, for: .normal)
        }
    }

    func printChildViewControllesInfo() {
        print("view.subviews.count: \(view.subviews.count)")
        print("childViewControllers.count: \(childViewControllers.count)")
    }
}

ViewControllerWithButton

import UIKit

class ViewControllerWithButton:UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    private func addButon() {
        let buttonWidth: CGFloat = 150
        let buttonHeight: CGFloat = 20
        let frame = CGRect(x: (view.frame.width-buttonWidth)/2, y: (view.frame.height-buttonHeight)/2, width: buttonWidth, height: buttonHeight)
        let button = UIButton(frame: frame)
        button.setTitle("Button", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        view.addSubview(button)
    }

    override func viewWillLayoutSubviews() {
        addButon()
    }

    @objc func buttonTapped() {
        print("Button tapped in \(self)")
    }
}

Results

enter image description here enter image description here enter image description here

Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
  • 1
    I have used this code to add `tableViewController` in a `viewController` but can not set the title of the former. I do not know if it is possible to do so. I have posted [this question](https://stackoverflow.com/questions/53134585/set-a-title-for-uitableviewcontroller-embedded-in-uiviewcontroller). It is nice of you if you have a look at it. – mahan Nov 03 '18 at 19:20