Como adicionar um Container View programaticamente

107

Uma Visualização de contêiner pode ser facilmente adicionada a um storyboard por meio do Editor de interface. Quando adicionada, uma Visualização de contêiner é uma visualização de espaço reservado, uma segue incorporada e um controlador de visualização (filho).

No entanto, não consigo encontrar uma maneira de adicionar um Container View programaticamente. Na verdade, não consigo nem encontrar uma classe chamada UIContainerViewou assim.

Um nome para a classe de Container View é certamente um bom começo. Um guia completo incluindo a segue será muito apreciado.

Conheço o View Controller Programming Guide, mas não o considero igual ao que o Interface Builder faz para o Container Viewer. Por exemplo, quando as restrições são definidas corretamente, a visualização (filha) se adapta às mudanças de tamanho na Visualização do contêiner.

Código Dante May
fonte
1
O que você quer dizer quando diz "quando as restrições são definidas corretamente, a visualização (filha) se adapta às mudanças de tamanho na Visualização de contêiner" (implicando assim que isso não é verdade quando você visualiza a contenção do controlador)? As restrições funcionam da mesma forma se você fez isso por meio de visualização de contêiner em IB ou visualização de contenção do controlador programaticamente.
Rob
1
O mais importante é o ViewControllerciclo de vida do embutido . O ViewControllerciclo de vida do embutido pelo Interface Builder é normal, mas aquele adicionado programaticamente não tem viewDidAppear, viewWillAppear(_:)nem viewWillDisappear.
DawnSong de
2
@DawnSong - Se você fizer as chamadas de contenção de visualização corretamente, o viewWillAppeare viewWillDisappearsão chamados no controlador de visualização filho, muito bem. Se você tiver um exemplo em que eles não estão, você deve esclarecer ou postar sua própria pergunta perguntando por que eles não são.
Rob

Respostas:

228

Uma "visualização de contêiner" de storyboard é apenas um UIViewobjeto padrão . Não existe um tipo especial de "visualização de contêiner". Na verdade, se você olhar para a hierarquia de visualizações, verá que a "visualização do contêiner" é um padrão UIView:

vista de contêiner

Para conseguir isso de forma programática, você emprega "visualizar contenção do controlador":

  • Instancie o controlador de visualização filho chamando instantiateViewController(withIdentifier:)o objeto storyboard.
  • Chame addChildseu controlador de visualização pai.
  • Adicione o controlador de visualização viewà sua hierarquia de visualização com addSubview(e também defina as framerestrições ou conforme apropriado).
  • Chame o didMove(toParent:)método no controlador de visualização filho, passando a referência para o controlador de visualização pai.

Consulte Implementing a Container View Controller no View Controller Programming Guide e a seção "Implementing a Container View Controller" da Referência de classe UIViewController .


Por exemplo, no Swift 4.2 pode ser parecido com:

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

Observe que o acima não adiciona realmente uma "visualização de contêiner" à hierarquia. Se você quiser fazer isso, você faria 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 padrão é extremamente útil se alguma vez fizer a transição entre diferentes controladores de visualização filho e você só quiser ter certeza de que a visualização de um filho está no mesmo local e a visualização do filho anterior (ou seja, todas as restrições exclusivas para a colocação são ditadas pela visualização do contêiner, em vez de precisar reconstruir essas restrições todas as vezes). Mas se apenas executar a contenção de visualização simples, a necessidade dessa visualização de contêiner separada é menos atraente.


Nos exemplos acima, eu estou definindo translatesAutosizingMaskIntoConstraintspara falsedefinir as restrições de mim mesmo. Você, obviamente, pode deixar translatesAutosizingMaskIntoConstraintscomo truee definir tanto o frameeo autosizingMaskpara os pontos de vista que você adicionar, se você preferir.


Veja as revisões anteriores desta resposta para versões de Swift 3 e Swift 2 .

Roubar
fonte
Não acho que sua resposta esteja completa. O mais importante é o ViewControllerciclo de vida do embutido . O ViewControllerciclo de vida do embutido pelo Interface Builder é normal, mas aquele adicionado programaticamente não tem viewDidAppear, viewWillAppear(_:)nem viewWillDisappear.
DawnSong de
Outra coisa estranha é que o incorporado ViewControllerde viewDidAppearé chamado no de seu pai viewDidLoad, em vez de durante o de seu paiviewDidAppear
DawnSong
@DawnSong - "mas aquele adicionado programaticamente tem viewDidAppear, [mas] nem, viewWillAppear(_:)nem viewWillDisappear". Os willmétodos de exibição são chamados corretamente em ambos os cenários. Deve-se chamar didMove(toParentViewController:_)ao fazê-lo programaticamente, embora, ou eles não o farão. Quanto ao tempo de aparecimento. métodos, eles são chamados na mesma sequência de ambas as maneiras. O que difere, entretanto, é o tempo de viewDidLoad, porque com embed, ele é carregado antes parent.viewDidLoad, mas com programático, como seria de se esperar, isso acontece durante parent.viewLoadLoad.
Rob de
2
Eu estava preso em restrições que não funcionavam; Acontece que eu estava faltando translatesAutoresizingMaskIntoConstraints = false. Não sei por que é necessário ou por que faz as coisas funcionarem, mas obrigado por incluí-lo em sua resposta.
hasen de
1
@Rob Em developer.apple.com/library/archive/featuredarticles/… na Listagem 5-1, há uma linha de código Objective-C que diz, "content.view.frame = [self frameForContentController];". O que é "frameForContentController" nesse código? Esse é o quadro da visualização do contêiner?
Daniel Brower
24

@ Resposta de Rob no 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 brilhante
fonte
13

Detalhes

  • Xcode 10.2 (10E125), Swift 5

Solução

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

Amostra 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

insira a descrição da imagem aqui insira a descrição da imagem aqui insira a descrição da imagem aqui

Vasily Bodnarchuk
fonte
1
Usei este código para adicionar tableViewControllerum, viewControllermas não consigo definir o título do anterior. Não sei se é possível fazer isso. Eu postei esta pergunta . É legal da sua parte se você der uma olhada nisso.
mahan
12

Aqui está meu código em 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
}

Use a outra função de incorporação com controlador de visualização não storyboard.

Jeffrey Chen
fonte
2
Ótima classe, no entanto, acho que preciso incorporar 2 viewControllers no mesmo controlador de exibição mestre, o que sua removeFromParentchamada impede, como você alteraria sua classe para permitir isso?
GarySabo
brilhante :) Obrigado
Rebeloper
É um bom exemplo, mas como posso adicionar algumas animações de transição a isso (incorporação, substituição de controladores de visualização filho)?
Michał Ziobro