如何以编程方式添加容器视图


107

容器视图可以通过Interface Editor轻松添加到情节提要中。添加后,“容器视图”将具有占位符视图,嵌入序列和(子)视图控制器。

但是,我无法找到以编程方式添加容器视图的方法。实际上,我什至找不到一个名为“ UIContainerViewso” 的类。

容器视图类的名称无疑是一个很好的开始。包括segue在内的完整指南将不胜感激。

我知道《 View Controller编程指南》,但是我不认为它与Interface Builder对Container Viewer的处理方式相同。例如,正确设置约束后,(子视图)将适应“容器视图”中的大小更改。


1
当您说“正确设置约束时,(子)视图将适应容器视图中的大小变化”(这意味着在执行视图控制器包含时,这不是真的)是什么意思?无论您是通过IB中的容器视图还是以编程方式通过视图控制器包含,约束的作用都相同。
罗布

1
最重要的是嵌入式ViewController的生命周期。ViewControllerInterface Builder 嵌入的生命周期是正常的,但是以编程方式添加的嵌入生命周期viewDidAppearviewWillAppear(_:)没有,也没有viewWillDisappear
DawnSong

2
@DawnSong-如果您正确地执行了视图包含调用,则在子视图控制器上调用viewWillAppearviewWillDisappear,就好了。如果您有一个示例,说明它们不是,请澄清或发布自己的问题。
罗布

Answers:


228

情节提要“容器视图”只是一个标准UIView对象。没有特殊的“容器视图”类型。实际上,如果您查看视图层次结构,则可以看到“容器视图”是标准的UIView

容器视图

要以编程方式实现此目的,可以使用“视图控制器包含”:

  • 通过调用instantiateViewController(withIdentifier:)情节提要对象实例化子视图控制器 。
  • 调用addChild父视图控制器。
  • 使用将视图控制器添加view到视图层次结构中addSubview(并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定义约束。显然,你可以离开translatesAutosizingMaskIntoConstraintstrue,并同时设置了frameautosizingMask为您添加,如果你愿意的意见。


有关Swift 3Swift 2演绎的信息,请参见此答案的先前版本。


我认为您的答案不完整。最重要的是嵌入式ViewController的生命周期。ViewControllerInterface Builder 嵌入的生命周期是正常的,但是以编程方式添加的嵌入生命周期viewDidAppearviewWillAppear(_:)没有,也没有viewWillDisappear
DawnSong

另一个奇怪的事情是,嵌入式ViewControllerviewDidAppear是在其父级中调用的viewDidLoad,而不是在其父级中viewDidAppear
调用

@DawnSong-“但是以编程方式添加的一个viewDidAppear没有,viewWillAppear(_:)也没有viewWillDisappear。” 该will显示方法在这两种情况下正确调用。以didMove(toParentViewController:_)编程方式进行操作时一定要打电话给别人,否则他们不会。关于出现的时间。方法,两种方法都以相同的顺序调用它们。因此,确实有所不同,viewDidLoad因为嵌入的时间是在之前加载的parent.viewDidLoad,而编程式的(正如我们期望的那样)是在的时间内进行的parent.viewLoadLoad
罗布

2
我因无法正常工作而受困;原来我不见了translatesAutoresizingMaskIntoConstraints = false。我不知道为什么需要它或为什么它使事情起作用,但是感谢您将其包含在答案中。
哈森

1
@Rob在清单5-1中的developer.apple.com/library/archive/featuredarticles/…处,有一行Objective-C代码,内容为:“ content.view.frame = [self frameForContentController];”。该代码中的“ frameForContentController”是什么?那是容器视图的框架吗?
丹尼尔·布劳

24

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

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

完整样本

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

结果

在此处输入图片说明 在此处输入图片说明 在此处输入图片说明


1
我已经使用此代码添加tableViewController了一个,viewController但无法设置前者的标题。我不知道是否可以这样做。我已经发布了这个问题。如果您有兴趣的话,对您很高兴。
mahan

12

这是我的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个viewController,这removeFromParent会阻止您的调用,您将如何修改类以允许这样做呢?
GarySabo

辉煌:)谢谢
Rebeloper'Mar 3'12

这是一个很好的示例,但是如何添加一些过渡动画(嵌入,替换子视图控制器)?
米哈尔Ziobro
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.