So fügen Sie programmgesteuert eine Containeransicht hinzu

107

Eine Containeransicht kann einfach über den Schnittstelleneditor zu einem Storyboard hinzugefügt werden. Beim Hinzufügen besteht eine Containeransicht aus einer Platzhalteransicht, einem Einbettungsabschnitt und einem (untergeordneten) Ansichtscontroller.

Ich kann jedoch keine Möglichkeit finden, eine Containeransicht programmgesteuert hinzuzufügen. Eigentlich kann ich nicht einmal eine Klasse mit dem Namen UIContainerViewoder so finden.

Ein Name für die Klasse der Containeransicht ist sicherlich ein guter Anfang. Eine vollständige Anleitung einschließlich des Segues wird sehr geschätzt.

Mir ist das View Controller-Programmierhandbuch bekannt, aber ich betrachte es nicht als dasselbe wie Interface Builder für Container Viewer. Wenn beispielsweise die Einschränkungen ordnungsgemäß festgelegt sind, wird die (untergeordnete) Ansicht an die Größenänderungen in der Containeransicht angepasst.

Dante May Code
quelle
1
Was meinen Sie, wenn Sie sagen "Wenn die Einschränkungen richtig festgelegt sind, passt sich die (untergeordnete) Ansicht an die Größenänderungen in der Containeransicht an" (was bedeutet, dass dies nicht der Fall ist, wenn Sie die Controller-Eindämmung anzeigen)? Einschränkungen funktionieren gleich, unabhängig davon, ob Sie dies über die Containeransicht in IB oder die programmgesteuerte Ansichtssteuerung getan haben.
Rob
1
Das Wichtigste ist der ViewControllerLebenszyklus des Embedded . Der ViewControllerLebenszyklus des Embedded durch Interface Builder ist normal, der programmgesteuert hinzugefügte jedoch viewDidAppearweder viewWillAppear(_:)noch viewWillDisappear.
DawnSong
2
@DawnSong - Wenn Sie die View Containment-Aufrufe korrekt ausführen, werden die viewWillAppearund viewWillDisappearauf dem untergeordneten View Controller aufgerufen. Wenn Sie ein Beispiel haben, in dem dies nicht der Fall ist, sollten Sie dies klären oder Ihre eigene Frage stellen, warum dies nicht der Fall ist.
Rob

Antworten:

228

Eine Storyboard- "Containeransicht" ist nur ein Standardobjekt UIView. Es gibt keinen speziellen Typ "Containeransicht". Wenn Sie sich die Ansichtshierarchie ansehen, sehen Sie, dass die "Containeransicht" ein Standard ist UIView:

Containeransicht

Um dies programmgesteuert zu erreichen, verwenden Sie "View Controller Containment":

  • Instanziieren Sie den untergeordneten Ansichtscontroller, indem Sie instantiateViewController(withIdentifier:)das Storyboard-Objekt aufrufen .
  • Rufen Sie addChildIhren übergeordneten View Controller auf.
  • Fügen Sie die Ansichts-Controller viewzu Ihrer Ansichtshierarchie hinzu addSubview(und legen Sie die frameoder Einschränkungen entsprechend fest).
  • Rufen Sie die didMove(toParent:)Methode auf dem untergeordneten Ansichtscontroller auf und übergeben Sie den Verweis an den übergeordneten Ansichtscontroller.

Siehe Implementieren eines Container View Controllers im View Controller-Programmierhandbuch und im Abschnitt "Implementieren eines Container View Controllers" der UIViewController-Klassenreferenz .


In Swift 4.2 könnte es beispielsweise so aussehen:

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

Beachten Sie, dass oben keine "Containeransicht" zur Hierarchie hinzugefügt wird. Wenn Sie das tun möchten, würden Sie etwas tun wie:

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

Dieses letztere Muster ist äußerst nützlich, wenn Sie jemals zwischen verschiedenen untergeordneten Ansichtscontrollern wechseln und nur sicherstellen möchten, dass sich die Ansicht eines Kindes am selben Speicherort befindet und die Ansicht des vorherigen Kindes (dh alle eindeutigen Einschränkungen für die Platzierung werden von der Containeransicht vorgegeben). anstatt diese Einschränkungen jedes Mal neu erstellen zu müssen). Wenn Sie jedoch nur eine einfache Ansichtsbegrenzung durchführen, ist die Notwendigkeit dieser separaten Containeransicht weniger zwingend.


In den obigen Beispielen, Ich gründe translatesAutosizingMaskIntoConstraintsauf falsedie Zwänge selbst zu definieren. Sie können offensichtlich verlassen translatesAutosizingMaskIntoConstraintswie trueund hier sowohl die frameund die autosizingMaskfür die Ansichten , die Sie hinzufügen möchten , wenn Sie bevorzugen würden.


Siehe frühere Überarbeitungen dieser Antwort für Swift 3- und Swift 2- Wiedergaben.

rauben
quelle
Ich denke nicht, dass Ihre Antwort vollständig ist. Das Wichtigste ist der ViewControllerLebenszyklus des Embedded . Der ViewControllerLebenszyklus des Embedded durch Interface Builder ist normal, der programmgesteuert hinzugefügte jedoch viewDidAppearweder viewWillAppear(_:)noch viewWillDisappear.
DawnSong
Eine weitere seltsame Sache ist , dass eingebettet ViewControllerist viewDidAppearin seinen Eltern genannt wird viewDidLoad, statt während seiner ElternviewDidAppear
DawnSong
@DawnSong - "aber der programmatisch hinzugefügte hat viewDidAppear, [aber] weder viewWillAppear(_:)noch viewWillDisappear". Die willangezeigten Methoden werden in beiden Szenarien korrekt aufgerufen. Man muss anrufen, didMove(toParentViewController:_)wenn man es programmgesteuert macht, sonst werden sie nicht. In Bezug auf den Zeitpunkt des Auftretens. Methoden werden sie in beide Richtungen in derselben Reihenfolge aufgerufen. Was sich jedoch unterscheidet, ist das Timing von viewDidLoad, denn mit Embed wird es vorher geladen parent.viewDidLoad, aber mit Programmatic, wie wir es erwarten würden, passiert es während parent.viewLoadLoad.
Rob
2
Ich war auf Einschränkungen fixiert, die nicht funktionierten. Es stellte sich heraus, dass ich vermisst wurde translatesAutoresizingMaskIntoConstraints = false. Ich weiß nicht, warum es gebraucht wird oder warum es funktioniert, aber ich danke Ihnen, dass Sie es in Ihre Antwort aufgenommen haben.
hasen
1
@Rob Unter developer.apple.com/library/archive/featuredarticles/… in Listing 5-1 befindet sich eine Zeile mit Objective-C-Code mit der Aufschrift "content.view.frame = [self frameForContentController];". Was ist "frameForContentController" in diesem Code? Ist das der Rahmen der Containeransicht?
Daniel Brower
24

@ Robs Antwort 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)
Strahlende Zukunft
quelle
13

Einzelheiten

  • Xcode 10.2 (10E125), Swift 5

Lösung

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

Verwendung

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

Vollständige Probe

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

Ergebnisse

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Wassili Bodnarchuk
quelle
1
Ich habe diesen Code verwendet, um tableViewControllereinen hinzuzufügen viewController, kann aber den Titel des ersteren nicht festlegen. Ich weiß nicht, ob das möglich ist. Ich habe diese Frage gestellt . Es ist nett von dir, wenn du es dir ansiehst.
Mahan
12

Hier ist mein Code 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)
}

}}

Verwendung

@IBOutlet weak var container:UIView!

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

Verwenden Sie die andere Einbettungsfunktion mit einem Nicht-Storyboard-Ansichts-Controller.

Jeffrey Chen
quelle
2
Tolle Klasse, aber ich muss 2 viewController in denselben Master-View-Controller einbetten, was Ihr removeFromParentAufruf verhindert. Wie würden Sie Ihre Klasse ändern, um dies zuzulassen?
Gary Sabo
brillant :) Danke
Rebeloper
Es ist ein schönes Beispiel, aber wie kann ich einige Übergangsanimationen hinzufügen (Einbetten, Ersetzen von untergeordneten Ansichtscontrollern)?
Michał Ziobro