Cómo agregar una vista de contenedor mediante programación

107

Una vista de contenedor se puede agregar fácilmente a un guión gráfico a través del editor de interfaz. Cuando se agrega, una vista de contenedor es una vista de marcador de posición, un segue incrustado y un controlador de vista (secundario).

Sin embargo, no puedo encontrar una manera de agregar una Vista de contenedor mediante programación. En realidad, ni siquiera puedo encontrar una clase con el nombre UIContainerViewo algo así.

Un nombre para la clase de Container View es sin duda un buen comienzo. Se agradecerá mucho una guía completa que incluya el segue.

Conozco la Guía de programación de View Controller, pero no la considero igual a la forma en que Interface Builder lo hace para Container Viewer. Por ejemplo, cuando las restricciones se establecen correctamente, la vista (secundaria) se adaptará a los cambios de tamaño en la Vista de contenedor.

Código Dante May
fuente
1
¿Qué quiere decir cuando dice "cuando las restricciones se establecen correctamente, la vista (secundaria) se adaptará a los cambios de tamaño en la Vista del contenedor" (lo que implica que esto no es cierto cuando ve la contención del controlador)? Las restricciones funcionan de la misma manera ya sea que lo hiciera a través de la vista de contenedor en IB o ver la contención del controlador mediante programación.
Rob
1
Lo más importante es el ViewControllerciclo de vida del incrustado . El ViewControllerciclo de vida del incrustado por Interface Builder es normal, pero el agregado mediante programación no tiene viewDidAppearni viewWillAppear(_:)ni viewWillDisappear.
DawnSong
2
@DawnSong: si realiza las llamadas de contención de vista correctamente, se invoca a viewWillAppeary viewWillDisappearen el controlador de vista secundario, está bien. Si tiene un ejemplo donde no lo son, debe aclararlo o publicar su propia pregunta preguntando por qué no lo son.
Rob

Respuestas:

228

Una "vista de contenedor" del guión gráfico es solo un UIViewobjeto estándar . No hay un tipo especial de "vista de contenedor". De hecho, si observa la jerarquía de vistas, puede ver que la "vista de contenedor" es un estándar UIView:

vista de contenedor

Para lograr esto de manera programática, emplea "contención del controlador de vista":

  • Cree una instancia del controlador de vista infantil llamando instantiateViewController(withIdentifier:)al objeto del guión gráfico.
  • Llame addChilda su controlador de vista de padres.
  • Agregue el controlador de vista viewa su jerarquía de vista con addSubview(y también establezca las framerestricciones o según corresponda).
  • Llame al didMove(toParent:)método en el controlador de vista secundario, pasando la referencia al controlador de vista principal.

Consulte Implementación de un controlador de vista de contenedor en la Guía de programación del controlador de vista y la sección "Implementación de un controlador de vista de contenedor" de la Referencia de clase de UIViewController .


Por ejemplo, en Swift 4.2 podría verse así:

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

Tenga en cuenta que lo anterior en realidad no agrega una "vista de contenedor" a la jerarquía. Si quieres hacer eso, harías algo como:

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

Este último patrón es extremadamente útil si alguna vez se realiza la transición entre diferentes controladores de vista infantil y solo desea asegurarse de que la vista de un niño esté en la misma ubicación y la vista del niño anterior (es decir, todas las restricciones únicas para la ubicación están dictadas por la vista del contenedor, en lugar de tener que reconstruir estas limitaciones cada vez). Pero si solo realiza una contención de vista simple, la necesidad de esta vista de contenedor separada es menos convincente.


En los ejemplos anteriores, me estoy configurando translatesAutosizingMaskIntoConstraintspara falsedefinir las restricciones yo mismo. Obviamente, puede dejar translatesAutosizingMaskIntoConstraintscomo truey establecer tanto el framecomo el autosizingMaskpara las vistas que agregue, si lo prefiere.


Consulte las revisiones anteriores de esta respuesta para las versiones de Swift 3 y Swift 2 .

Robar
fuente
No creo que tu respuesta esté completa. Lo más importante es el ViewControllerciclo de vida del incrustado . El ViewControllerciclo de vida del incrustado por Interface Builder es normal, pero el agregado mediante programación no tiene viewDidAppearni viewWillAppear(_:)ni viewWillDisappear.
DawnSong
Otra cosa extraña es que incrustado ViewController's viewDidAppearque se llama en su matriz de viewDidLoad, en lugar de durante su del padreviewDidAppear
DawnSong
@DawnSong - "pero el agregado programáticamente tiene viewDidAppear, [pero] ni viewWillAppear(_:)ni viewWillDisappear". Los willmétodos de aparecer se llaman correctamente en ambos escenarios. Uno debe llamar didMove(toParentViewController:_)al hacerlo mediante programación, aunque, de lo contrario, no lo harán. Respecto al momento de la aparición. métodos, se llaman en la misma secuencia en ambos sentidos. Lo que sí difiere, sin embargo, es el tiempo de viewDidLoad, porque con la inserción, se carga antes parent.viewDidLoad, pero con la programática, como era de esperar, ocurre durante parent.viewLoadLoad.
Rob
2
Estaba atascado en restricciones que no funcionaban; resulta que me faltaba translatesAutoresizingMaskIntoConstraints = false. No sé por qué es necesario o por qué hace que las cosas funcionen, pero gracias por incluirlo en su respuesta.
Hasen
1
@Rob En developer.apple.com/library/archive/featuredarticles/… en el Listado 5-1, hay una línea de código Objective-C que dice, "content.view.frame = [self frameForContentController];". ¿Qué es "frameForContentController" en ese código? ¿Ese es el marco de la vista del contenedor?
Daniel Brower
24

La respuesta de @ Rob en 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)
Futuro brillante
fuente
13

Detalles

  • Xcode 10.2 (10E125), Swift 5

Solución

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

Uso

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

Muestra completa

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

Resultados

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Vasily Bodnarchuk
fuente
1
He usado este código para agregar tableViewControllerun viewControllerpero no puedo establecer el título del primero. No sé si es posible hacerlo. He publicado esta pregunta . Es amable de su parte si le echa un vistazo.
mahan
12

Aquí está mi código en 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)
}

}

Uso

@IBOutlet weak var container:UIView!

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

Utilice la otra función de incrustación con el controlador de vista sin guión gráfico.

Jeffrey Chen
fuente
2
Gran clase, sin embargo, me encuentro en la necesidad de incrustar 2 viewControllers dentro del mismo controlador de vista maestro, lo que su removeFromParentllamada evita, ¿cómo enmendaría su clase para permitir esto?
GarySabo
brillante :) Gracias
Rebeloper
Es un buen ejemplo, pero ¿cómo puedo agregar algunas animaciones de transición a esto (incrustación, reemplazo de controladores de vista secundaria)?
Michał Ziobro