如何通过编程方式添加一个容器视图

144

通过接口编辑器,可以很容易地在故事板中添加容器视图。添加后,容器视图是一个占位符视图、嵌入式segue和一个(子)视图控制器。

然而,我无法找到以编程方式添加容器视图的方法。实际上,我甚至无法找到名为UIContainerView的类或类似的类。

了解容器视图的类名肯定是一个好的开始。包括Segue在内的完整指南将不胜感激。

我知道有关视图控制器编程指南,但我认为它与Interface Builder为容器视图提供的方式不同。例如,当正确设置约束时,(子)视图将适应容器视图中的大小更改。


1
当你说“当约束正确设置时,(子)视图将适应容器视图中的大小更改”时,你的意思是什么?(从而暗示在视图控制器包含时这不是真的)。无论是通过IB中的容器视图还是编程方式的视图控制器包含,约束都是相同的。 - Rob
1
最重要的是嵌入式“ViewController”的生命周期。通过Interface Builder嵌入的“ViewController”生命周期正常,但通过编程方式添加的“ViewController”具有“viewDidAppear”,而没有“viewWillAppear(_ :)”或“viewWillDisappear”。 - DawnSong
2
@DawnSong - 如果你正确地进行了视图包含调用,viewWillAppearviewWillDisappear将会在子视图控制器上被正确调用。如果你有一个例子它们没有被调用,你应该澄清一下,或者发布自己的问题询问为什么它们没有被调用。 - Rob
4个回答

278

一个故事板的"容器视图"只是一个标准的UIView对象。 没有特殊的"容器视图"类型。 实际上,如果您查看视图层次结构,您会发现 "容器视图" 是一个标准的UIView

container view

要在程序中实现这一点,您需要使用"视图控制器包含":

  • 通过在故事板对象上调用 instantiateViewController(withIdentifier:) 来实例化子视图控制器。
  • 在父视图控制器中调用 addChild
  • 使用addSubview将视图控制器的 view 添加到视图层次结构中(并根据需要设置frame或约束)。
  • 在子视图控制器上调用 didMove(toParent:) 方法,并传递对父视图控制器的引用。

请参阅 视图控制器编程指南中的"实现容器视图控制器" 部分以及 UIViewController 类参考中的 "实现容器视图控制器" 部分。


例如,在 Swift 4.2 中,代码可能如下所示:

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)
}

请注意,以上实际上并没有向层次结构中添加一个“容器视图”。如果您想这样做,可以执行以下操作:

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)
}

如果要在不同的子视图控制器之间进行转换,并且您只想确保一个子视图的位置与上一个子视图的视图相同(即所有唯一放置约束都由容器视图指定,而无需每次重新构建这些约束),那么后一种模式非常有用。但是,如果只执行简单的视图包容性,则对于这个单独的容器视图的需求就不那么强烈。


在上面的示例中,我将translatesAutosizingMaskIntoConstraints设置为false并自己定义了约束条件。当然,您也可以将translatesAutosizingMaskIntoConstraints保留为true并为添加的视图同时设置frameautosizingMask,如果您愿意。


请查看此答案的先前版本,了解 Swift 3Swift 2 版本。


1
我认为你的答案不完整。最重要的是嵌入式“ViewController”的生命周期。通过Interface Builder添加的嵌入式“ViewController”生命周期正常,但通过编程方式添加的“ViewController”只有“viewDidAppear”,没有“viewWillAppear(_ :)”或“viewWillDisappear”。 - DawnSong
2
我被约束条件卡住了,结果发现我漏掉了 translatesAutoresizingMaskIntoConstraints = false。我不知道为什么需要它或者为什么它能让事情正常工作,但感谢您在答案中包含它。 - hasen
1
在https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html的Rob At中,第5-1列表中有一行Objective-C代码,其中写道“content.view.frame = [self frameForContentController];”。那么,在该代码中,“frameForContentController”是什么?那是容器视图的框架吗? - daniel
1
谢谢!我一直在尝试在XCode中使用外部故事板来完成这个(任务),但是UI似乎没有给我设置segue的方法。在我的情况下,通过编程实现是可以的,你的解决方案非常好用。 - Jc Nolan
1
在以编程方式添加视图时,记住 translatesAutoresizingMaskIntoConstraints = false 是一件重要的事情。 - Manish Singh
显示剩余8条评论

29

@Rob在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)

19

这是我的 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)
}

}

使用方法

@IBOutlet weak var container:UIView!

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

使用其他嵌入功能与非Storyboard视图控制器。


2
这是一个很棒的类,但我发现自己需要在同一个主视图控制器中嵌入2个视图控制器,而您的removeFromParent调用会阻止这一点。您如何修改您的类以允许这种情况? - GarySabo
杰出的 :) 谢谢 - Rebeloper
这是一个不错的例子,但我该如何为它添加一些过渡动画(嵌入、替换子视图控制器)呢? - Michał Ziobro

13

详情

  • Xcode 10.2 (10E125), Swift 5

解决方案

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()
    }
}

使用方法

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)
    }
}

完整示例

视图控制器

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)")
    }
}

带按钮的视图控制器

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)")
    }
}

结果

图片描述 图片描述 图片描述


1
我已经使用这段代码在一个视图控制器中添加了tableViewController,但是无法设置前者的标题。我不知道是否有可能这样做。我已经发布了这个问题,如果您能看一下就太好了。 - mahan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接