Come aggiungere una visualizzazione contenitore a livello di codice


107

Una visualizzazione contenitore può essere facilmente aggiunta a uno storyboard tramite l'editor dell'interfaccia. Quando viene aggiunta, una visualizzazione contenitore è composta da una visualizzazione segnaposto, un segue da incorporamento e un controller di visualizzazione (figlio).

Tuttavia, non riesco a trovare un modo per aggiungere una visualizzazione contenitore a livello di programmazione. In realtà, non sono nemmeno in grado di trovare una classe chiamata UIContainerViewo giù di lì.

Un nome per la classe di Container View è sicuramente un buon inizio. Una guida completa che includa il seguito sarà molto apprezzata.

Sono a conoscenza della Guida alla programmazione di View Controller, ma non la considero uguale a quella che fa Interface Builder per Container Viewer. Ad esempio, quando i vincoli sono impostati correttamente, la vista (figlia) si adatterà ai cambiamenti di dimensione nella vista contenitore.


1
Cosa intendi quando dici "quando i vincoli sono impostati correttamente, la vista (figlia) si adatterà ai cambiamenti di dimensione nella vista contenitore" (implicando così che questo non è vero quando visualizzi il contenimento del controller)? I vincoli funzionano allo stesso modo sia che tu lo abbia fatto tramite la visualizzazione contenitore in IB o il contenimento del controller di visualizzazione a livello di codice.
Rob

1
La cosa più importante è il ViewControllerciclo di vita dell'integrato . Il ViewControllerciclo di vita dell'integrato di Interface Builder è normale, ma quello aggiunto a livello di programmazione non ha viewDidAppearviewWillAppear(_:)viewWillDisappear.
DawnSong

2
@DawnSong - Se esegui correttamente le chiamate di contenimento della vista, viewWillAppeare viewWillDisappearvengono chiamati sul controller della vista figlio, benissimo. Se hai un esempio in cui non lo sono, dovresti chiarire o pubblicare la tua domanda chiedendo perché non lo sono.
Rob

Risposte:


228

Una "visualizzazione contenitore" dello storyboard è solo un UIViewoggetto standard . Non esiste un tipo speciale di "visualizzazione contenitore". Infatti, se guardi la gerarchia delle viste, puoi vedere che la "vista contenitore" è uno standard UIView:

vista contenitore

Per ottenere questo risultato a livello di codice, utilizzi il "contenimento del controller di visualizzazione":

  • Crea un'istanza del controller della visualizzazione figlio chiamando instantiateViewController(withIdentifier:)l'oggetto storyboard.
  • Chiama addChildil controller della vista genitore.
  • Aggiungi il controller di viewvisualizzazione alla gerarchia di visualizzazione con addSubview(e imposta anche i framevincoli o come appropriato).
  • Chiama il didMove(toParent:)metodo sul controller della vista figlio, passando il riferimento al controller della vista padre.

Vedere Implementazione di un controller di visualizzazione del contenitore nella Guida alla programmazione del controller di visualizzazione e la sezione "Implementazione di un controller di visualizzazione del contenitore" del riferimento alla classe UIViewController .


Ad esempio, in Swift 4.2 potrebbe apparire come:

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

Nota, quanto sopra in realtà non aggiunge una "visualizzazione contenitore" alla gerarchia. Se vuoi farlo, faresti qualcosa come:

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

Quest'ultimo modello è estremamente utile se si passa da un controller di visualizzazione figlio a un altro e si desidera solo assicurarsi che la visualizzazione di un bambino si trovi nella stessa posizione e la visualizzazione del bambino precedente (cioè tutti i vincoli unici per il posizionamento sono dettati dalla visualizzazione contenitore, piuttosto che dover ricostruire ogni volta questi vincoli). Ma se si esegue solo il contenimento della vista semplice, la necessità di questa vista contenitore separata è meno convincente.


Negli esempi precedenti, mi sto preparando translatesAutosizingMaskIntoConstraintsa falsedefinire i vincoli da solo. Ovviamente puoi lasciare translatesAutosizingMaskIntoConstraintscome truee impostare sia il frameche il autosizingMaskper le visualizzazioni che aggiungi, se preferisci.


Vedi le revisioni precedenti di questa risposta per le versioni di Swift 3 e Swift 2 .


Non credo che la tua risposta sia completa. La cosa più importante è il ViewControllerciclo di vita dell'integrato . Il ViewControllerciclo di vita dell'integrato di Interface Builder è normale, ma quello aggiunto a livello di programmazione non ha viewDidAppearviewWillAppear(_:)viewWillDisappear.
DawnSong

Un'altra cosa strana è che embedded ViewController's viewDidAppearè chiamato in quello dei suoi genitori viewDidLoad, invece che durante i suoi genitoriviewDidAppear
DawnSong

@DawnSong - "ma quello aggiunto a livello di codice ha viewDidAppear, [ma] né viewWillAppear(_:)viewWillDisappear ". I willmetodi di visualizzazione vengono chiamati correttamente in entrambi gli scenari. Uno deve chiamare didMove(toParentViewController:_)quando lo si fa a livello di programmazione, anche se altrimenti non lo faranno. Per quanto riguarda i tempi dell'apparizione. metodi, vengono chiamati nella stessa sequenza in entrambi i modi. Ciò che differisce, tuttavia, è il tempismo viewDidLoad, perché con l'incorporamento viene caricato prima parent.viewDidLoad, ma con il programmatico, come ci aspetteremmo, accade durante parent.viewLoadLoad.
Rob il

2
Ero bloccato su vincoli che non funzionavano; si scopre che mi mancava translatesAutoresizingMaskIntoConstraints = false. Non so perché sia ​​necessario o perché faccia funzionare le cose, ma grazie per averlo incluso nella tua risposta.
hasen

1
@Rob In developer.apple.com/library/archive/featuredarticles/… nel Listato 5-1, c'è una riga di codice Objective-C che dice "content.view.frame = [self frameForContentController];". Che cos'è "frameForContentController" in quel codice? È questo il frame della visualizzazione contenitore?
Daniel Brower

24

La risposta di @ Rob 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)

13

Dettagli

  • Xcode 10.2 (10E125), Swift 5

Soluzione

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

Campione completo

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

risultati

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


1
Ho usato questo codice per aggiungere tableViewControllerun viewControllerma non posso impostare il titolo del primo. Non so se sia possibile farlo. Ho pubblicato questa domanda . È carino da parte tua se lo dai un'occhiata.
mahan

12

Ecco il mio codice 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)
}

}

uso

@IBOutlet weak var container:UIView!

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

Usa l'altra funzione di incorporamento con un controller di visualizzazione non storyboard.


2
Ottima classe, tuttavia mi trovo necessario per incorporare 2 viewController nello stesso controller della vista master, cosa che la tua removeFromParentchiamata impedisce, come modificheresti la tua classe per consentirlo?
GarySabo

brillante :) Grazie
Rebeloper

È un bell'esempio, ma come posso aggiungere alcune animazioni di transizione a questo (incorporamento, sostituzione dei controller di visualizzazione figlio)?
Michał Ziobro
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.