Comment ajouter une vue de conteneur par programme

107

Une vue de conteneur peut être facilement ajoutée dans un storyboard via l'éditeur d'interface. Lorsqu'elle est ajoutée, une vue de conteneur est une vue d'espace réservé, une séquence incorporée et un contrôleur de vue (enfant).

Cependant, je ne parviens pas à trouver un moyen d'ajouter une vue de conteneur par programmation. En fait, je ne suis même pas capable de trouver une classe nommée UIContainerViewou ainsi.

Un nom pour la classe de Container View est certainement un bon début. Un guide complet comprenant la suite sera très apprécié.

Je connais le Guide de programmation de View Controller, mais je ne le considère pas comme identique à la manière dont Interface Builder le fait pour Container Viewer. Par exemple, lorsque les contraintes sont correctement définies, la vue (enfant) s'adapte aux changements de taille dans Container View.

Code de Dante May
la source
1
Que voulez-vous dire lorsque vous dites "lorsque les contraintes sont correctement définies, la vue (enfant) s'adaptera aux changements de taille dans Container View" (ce qui implique que ce n'est pas vrai lorsque vous visualisez le confinement du contrôleur)? Les contraintes fonctionnent de la même manière que vous l'ayez fait via la vue de conteneur dans IB ou le confinement du contrôleur de vue par programmation.
Rob
1
Le plus important est le ViewControllercycle de vie de l ' embarqué . Le ViewControllercycle de vie de l ' embarqué par Interface Builder est normal, mais celui ajouté par programmation viewDidAppearne l' a viewWillAppear(_:)ni ni ni viewWillDisappear.
DawnSong
2
@DawnSong - Si vous effectuez correctement les appels de confinement de vue, les viewWillAppearet viewWillDisappearsont appelés sur le contrôleur de vue enfant, très bien. Si vous avez un exemple où ils ne le sont pas, vous devriez clarifier ou poster votre propre question demandant pourquoi ils ne le sont pas.
Rob

Réponses:

228

Une "vue conteneur" de storyboard n'est qu'un UIViewobjet standard . Il n'y a pas de type spécial "vue conteneur". En fait, si vous regardez la hiérarchie des vues, vous pouvez voir que la "vue conteneur" est un standard UIView:

vue du conteneur

Pour y parvenir par programme, vous utilisez le «confinement du contrôleur de vue»:

  • Instanciez le contrôleur de vue enfant en appelant instantiateViewController(withIdentifier:)l'objet storyboard.
  • Appelez addChildvotre contrôleur de vue parent.
  • Ajoutez le contrôleur de vue viewà votre hiérarchie de vues avec addSubview(et définissez également les framecontraintes ou selon le cas).
  • Appelez la didMove(toParent:)méthode sur le contrôleur de vue enfant, en transmettant la référence au contrôleur de vue parent.

Consultez Implémentation d'un contrôleur de vue de conteneur dans le Guide de programmation de contrôleur de vue et la section «Implémentation d'un contrôleur de vue de conteneur» de la référence de classe UIViewController .


Par exemple, dans Swift 4.2, cela pourrait ressembler à:

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

Notez que ce qui précède n'ajoute pas réellement une "vue conteneur" à la hiérarchie. Si vous voulez faire cela, vous feriez quelque chose comme:

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

Ce dernier modèle est extrêmement utile en cas de transition entre différents contrôleurs de vue enfant et vous voulez simplement vous assurer que la vue d'un enfant est au même endroit et que la vue de l'enfant précédent (c'est-à-dire que toutes les contraintes uniques pour le placement sont dictées par la vue du conteneur, plutôt que de devoir reconstruire ces contraintes à chaque fois). Mais si vous effectuez simplement un confinement de vue simple, le besoin de cette vue de conteneur séparée est moins convaincant.


Dans les exemples ci-dessus, je me propose translatesAutosizingMaskIntoConstraintsde falsedéfinir moi-même les contraintes. Vous pouvez évidemment laisser translatesAutosizingMaskIntoConstraintscomme trueet définir à la fois le frameet le autosizingMaskpour les vues que vous ajoutez, si vous préférez.


Voir les révisions précédentes de cette réponse pour les rendus Swift 3 et Swift 2 .

Rob
la source
Je ne pense pas que votre réponse soit complète. Le plus important est le ViewControllercycle de vie de l ' embarqué . Le ViewControllercycle de vie de l ' embarqué par Interface Builder est normal, mais celui ajouté par programmation viewDidAppearne l' a viewWillAppear(_:)ni ni ni viewWillDisappear.
DawnSong
Une autre chose étrange est que intégré ViewController« s viewDidAppearest appelé dans son parent de viewDidLoad, au lieu de son cours de parentviewDidAppear
DawnSong
@DawnSong - "mais celui ajouté par programme a viewDidAppear, [mais] ni viewWillAppear(_:)ni viewWillDisappear". Les willméthodes d'apparition sont appelées correctement dans les deux scénarios. Il faut appeler didMove(toParentViewController:_)quand on le fait par programme, mais sinon ils ne le feront pas. En ce qui concerne le moment de l'apparition. méthodes, elles sont appelées dans le même ordre dans les deux sens. Ce qui diffère, cependant, c'est le timing de viewDidLoad, car avec l'intégration, il est chargé avant parent.viewDidLoad, mais avec le programmatique, comme on pouvait s'y attendre, cela se produit pendant parent.viewLoadLoad.
Rob
2
J'étais coincé sur des contraintes qui ne fonctionnaient pas; s'avère que je manquais translatesAutoresizingMaskIntoConstraints = false. Je ne sais pas pourquoi c'est nécessaire ni pourquoi cela fait fonctionner les choses, mais merci de l'inclure dans votre réponse.
hasen le
1
@Rob Sur developer.apple.com/library/archive/featuredarticles/… dans le Listing 5-1, il y a une ligne de code Objective-C qui dit, "content.view.frame = [self frameForContentController];". Qu'est-ce que "frameForContentController" dans ce code? Est-ce le cadre de la vue du conteneur?
Daniel Brower
24

@ Réponse de Rob dans 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)
Brillant avenir
la source
13

Détails

  • Xcode 10.2 (10E125), Swift 5

Solution

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

Usage

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

Échantillon complet

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

Résultats

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

Vasily Bodnarchuk
la source
1
J'ai utilisé ce code pour ajouter tableViewControllerun viewControllermais je ne peux pas définir le titre de l'ancien. Je ne sais pas s'il est possible de le faire. J'ai posté cette question . C'est gentil de ta part si tu y jettes un œil.
mahan
12

Voici mon code dans 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)
}

}

Usage

@IBOutlet weak var container:UIView!

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

Utilisez l'autre fonction d'intégration avec un contrôleur de vue sans storyboard.

Jeffrey Chen
la source
2
Excellente classe, mais je me trouve obligé d'incorporer 2 viewControllers dans le même contrôleur de vue maître, ce que votre removeFromParentappel empêche, comment modifieriez-vous votre classe pour permettre cela?
GarySabo
brillant :) Merci
Rebeloper
C'est un bel exemple, mais comment puis-je ajouter des animations de transition à cela (intégration, remplacement des contrôleurs de vue enfants)?
Michał Ziobro