Jak programowo dodać widok kontenera

107

Widok kontenera można łatwo dodać do scenorysu za pomocą edytora interfejsu. Po dodaniu widok kontenera jest widokiem zastępczym, osadzonym płynem i (podrzędnym) kontrolerem widoku.

Nie mogę jednak znaleźć sposobu na programowe dodanie widoku kontenera. Właściwie nie jestem nawet w stanie znaleźć klasy o UIContainerViewtakiej nazwie .

Nazwa klasy Container View to z pewnością dobry początek. Kompletny przewodnik zawierający odcinek będzie bardzo mile widziany.

Znam Przewodnik programowania kontrolera, ale nie uważam go za taki sam, jak robi to Interface Builder dla Container Viewer. Na przykład, gdy ograniczenia są prawidłowo ustawione, widok (podrzędny) dostosuje się do zmian rozmiaru w widoku kontenera.

Kod Dante May
źródło
1
Co masz na myśli, mówiąc „gdy ograniczenia są prawidłowo ustawione, widok (podrzędny) dostosuje się do zmian rozmiaru w widoku kontenera” (co oznacza, że ​​nie jest to prawdą, gdy wyświetlasz zawartość kontrolera)? Ograniczenia działają tak samo, niezależnie od tego, czy zostało to zrobione za pośrednictwem widoku kontenera w IB, czy też programowo zawiera kontroler widoku.
Rob
1
Najważniejszy jest ViewControllercykl życia elementów wbudowanych . ViewControllerCykl życia osadzonego przez Interface Builder jest normalny, ale ten dodany programowo nie ma viewDidAppearani viewWillAppear(_:)ani viewWillDisappear.
DawnSong
2
@DawnSong - Jeśli poprawnie wykonasz wywołania zawartości widoku, viewWillAppeari viewWillDisappearsą wywoływane na kontrolerze widoku podrzędnego, w porządku. Jeśli masz przykład, w którym ich nie ma, powinieneś wyjaśnić lub zadać własne pytanie, pytając, dlaczego tak nie jest.
Rob

Odpowiedzi:

228

„Widok kontenera” scenorysu to tylko standardowy UIViewobiekt. Nie ma specjalnego typu „widoku kontenera”. W rzeczywistości, jeśli spojrzysz na hierarchię widoków, zobaczysz, że „widok kontenera” jest standardem UIView:

widok kontenera

Aby osiągnąć to programowo, stosujesz „ograniczenie kontrolera widoku”:

  • Utwórz wystąpienie kontrolera widoku podrzędnego, wywołując instantiateViewController(withIdentifier:)obiekt serii ujęć.
  • Zadzwoń addChilddo kontrolera widoku rodzica.
  • Dodaj kontroler widoku viewdo hierarchii widoku za pomocą addSubview(a także ustaw odpowiednio frameograniczenia lub).
  • Wywołaj didMove(toParent:)metodę na podrzędnym kontrolerze widoku, przekazując odwołanie do nadrzędnego kontrolera widoku.

Zobacz implementowanie kontrolera widoku kontenera w Przewodniku programowania kontrolera widoku i sekcji „Implementowanie kontrolera widoku kontenera” w odwołaniu do klasy UIViewController .


Na przykład w Swift 4.2 może wyglądać tak:

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

Uwaga, powyższe w rzeczywistości nie dodaje „widoku kontenera” do hierarchii. Jeśli chcesz to zrobić, zrób coś takiego:

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

Ten ostatni wzorzec jest niezwykle przydatny, jeśli kiedykolwiek przechodzisz między różnymi kontrolerami widoku podrzędnego i chcesz się tylko upewnić, że widok jednego dziecka znajduje się w tej samej lokalizacji i widoku poprzedniego dziecka (tj. Wszystkie unikalne ograniczenia dla miejsca docelowego są podyktowane przez widok kontenera, zamiast za każdym razem przebudowywać te ograniczenia). Ale jeśli wykonujesz tylko proste zawarcie widoku, potrzeba tego oddzielnego widoku kontenera jest mniej przekonująca.


W powyższych przykładach zamierzam translatesAutosizingMaskIntoConstraintssamodzielnie falsezdefiniować ograniczenia. Oczywiście możesz pozostawić translatesAutosizingMaskIntoConstraintsjako truei ustawić zarówno widok, jak framei autosizingMaskdla dodawanych widoków, jeśli wolisz.


Zobacz poprzednie wersje tej odpowiedzi dla wersji Swift 3 i Swift 2 .

Obrabować
źródło
Nie sądzę, że twoja odpowiedź jest kompletna. Najważniejszy jest ViewControllercykl życia elementów wbudowanych . ViewControllerCykl życia osadzonego przez Interface Builder jest normalny, ale ten dodany programowo nie ma viewDidAppearani viewWillAppear(_:)ani viewWillDisappear.
DawnSong
Kolejną dziwną rzeczą jest to, że osadzony ViewController„y viewDidAppearnazywa się w jego rodziców viewDidLoad, a nie podczas jego rodzicówviewDidAppear
DawnSong
@DawnSong - "ale ten dodany programowo ma viewDidAppear, [ale] ani viewWillAppear(_:)ani viewWillDisappear". Wyświetlane willmetody są wywoływane poprawnie w obu scenariuszach. Trzeba zadzwonić didMove(toParentViewController:_), robiąc to programowo, ale inaczej nie zrobią. Odnośnie czasu pojawienia się. metody są wywoływane w tej samej kolejności w obie strony. Różni się jednak czas viewDidLoad, ponieważ w przypadku osadzania jest ładowany wcześniej parent.viewDidLoad, ale w przypadku programmatic, jak można się spodziewać, dzieje się to podczas parent.viewLoadLoad.
Rob
2
Utknąłem na niedziałających ograniczeniach; okazuje się, że mi brakowało translatesAutoresizingMaskIntoConstraints = false. Nie wiem, dlaczego jest to potrzebne ani dlaczego sprawia, że ​​wszystko działa, ale dziękuję za uwzględnienie tego w odpowiedzi.
hasen
1
@Rob Na stronie developer.apple.com/library/archive/feratedarticles/ ... na listingu 5-1 znajduje się wiersz kodu Objective-C, który mówi: „content.view.frame = [self frameForContentController];”. Co to jest „frameForContentController” w tym kodzie? Czy to jest rama widoku kontenera?
Daniel Brower,
24

@ Odpowiedź Roba w 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)
Świetlana przyszłość
źródło
13

Detale

  • Xcode 10.2 (10E125), Swift 5

Rozwiązanie

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

Stosowanie

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

Pełna próbka

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

Wyniki

wprowadź opis obrazu tutaj wprowadź opis obrazu tutaj wprowadź opis obrazu tutaj

Wasilij Bodnarczuk
źródło
1
Użyłem tego kodu, aby dodać tableViewControllerw sposób viewController, ale nie można ustawić tytuł pierwszego. Nie wiem, czy da się to zrobić. Wysłałem to pytanie . Miło z twojej strony, jeśli na to spojrzysz.
mahan
12

Oto mój kod w 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)
}

}

Stosowanie

@IBOutlet weak var container:UIView!

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

Użyj drugiej funkcji osadzania z kontrolerem widoku innym niż scenorys.

Jeffrey Chen
źródło
2
Świetna klasa, jednak muszę osadzić 2 kontrolery viewControllers w tym samym kontrolerze widoku głównego, czemu removeFromParentzapobiega twoje wywołanie, jak zmodyfikowałbyś swoją klasę, aby na to zezwolić?
GarySabo
genialne :) Dziękuję
Rebeloper
To fajny przykład, ale jak mogę dodać do tego animacje przejścia (emulacja, zamiana podrzędnych kontrolerów widoku)?
Michał Ziobro