Как программно добавить представление контейнера

108

Представление контейнера можно легко добавить в раскадровку с помощью редактора интерфейса. При добавлении представление контейнера имеет представление заполнителя, переход для встраивания и (дочерний) контроллер представления.

Однако я не могу найти способ программно добавить представление контейнера. На самом деле я даже не могу найти класс с таким именем UIContainerView.

Название класса Container View, несомненно, станет хорошим началом. Будем очень признательны за полное руководство, включая переход.

Мне известно о руководстве по программированию контроллера представления, но я не считаю его таким же, как интерфейс Builder для средства просмотра контейнеров. Например, если ограничения установлены правильно, (дочернее) представление адаптируется к изменениям размера в представлении контейнера.

Код Данте Май
источник
1
Что вы имеете в виду, когда говорите «когда ограничения установлены правильно, (дочернее) представление будет адаптироваться к изменениям размера в представлении контейнера» (тем самым подразумевая, что это неверно, когда вы просматриваете включение контроллера)? Ограничения работают одинаково, независимо от того, сделали ли вы это через представление контейнера в IB или программно.
Роб
1
Самое главное - это ViewControllerжизненный цикл встроенного . ViewControllerЖизненный цикл встроенного с помощью Interface Builder нормальный, но тот, который добавлен программно viewDidAppear, не имеет ни, viewWillAppear(_:)ни viewWillDisappear.
DawnSong
2
@DawnSong - Если вы правильно выполняете вызовы включения представления , то viewWillAppearи viewWillDisappearвызываются на дочернем контроллере представления, нормально. Если у вас есть пример, в котором их нет, вы должны уточнить или опубликовать свой вопрос, спрашивая, почему это не так.
Роб

Ответы:

230

«Контейнерное представление» раскадровки - это просто стандартный UIViewобъект. Специального типа "контейнерный вид" нет. Фактически, если вы посмотрите на иерархию представлений, вы увидите, что «контейнерное представление» является стандартом UIView:

вид контейнера

Чтобы достичь этого программно, вы используете «сдерживание контроллера представления»:

  • Создайте экземпляр дочернего контроллера представления, вызвав instantiateViewController(withIdentifier:)объект раскадровки.
  • Вызовите addChildсвой родительский контроллер представления.
  • Добавьте контроллер представления viewв иерархию представления с помощью addSubview(а также установите соответствующие frameограничения или).
  • Вызовите didMove(toParent:)метод на контроллере дочернего представления, передав ссылку на контроллер родительского представления.

См. Раздел « Реализация контроллера представления контейнера» в Руководстве по программированию контроллера представления. и раздел «Реализация контроллера представления контейнера» справочника по классам UIViewController .


Например, в Swift 4.2 это может выглядеть так:

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

Обратите внимание, что приведенное выше на самом деле не добавляет к иерархии «представление контейнера». Если вы хотите это сделать, сделайте что-то вроде:

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

Этот последний шаблон чрезвычайно полезен при переходе между различными контроллерами дочерних представлений, и вы просто хотите убедиться, что одно дочернее представление находится в том же месте, что и предыдущее дочернее представление (т.е. все уникальные ограничения для размещения продиктованы представлением контейнера, вместо того, чтобы каждый раз перестраивать эти ограничения). Но если просто выполнить простое включение представления, потребность в этом отдельном представлении контейнера будет менее острой.


В приведенных выше примерах, я устанавливаю translatesAutosizingMaskIntoConstraintsдля falseопределения ограничений себя. Вы, очевидно, можете оставить translatesAutosizingMaskIntoConstraintsкак trueи установить и frameи autosizingMaskдля добавляемых вами представлений, если хотите.


См. Предыдущие версии этого ответа для представлений Swift 3 и Swift 2 .

Роб
источник
Я не думаю, что ваш ответ исчерпывающий. Самое главное - это ViewControllerжизненный цикл встроенного . ViewControllerЖизненный цикл встроенного с помощью Interface Builder нормальный, но тот, который добавлен программно viewDidAppear, не имеет ни, viewWillAppear(_:)ни viewWillDisappear.
DawnSong
Еще одна странная вещь в том , что встроенный ViewController«ы viewDidAppearназывается в его родителя viewDidLoad, вместо того , чтобы во время своего родителяviewDidAppear
DawnSong
@DawnSong - «но тот, который добавлен программно, не имеет viewDidAppear, [но] ни viewWillAppear(_:)или viewWillDisappear». В willобоих сценариях методы появления вызываются правильно. При этом нужно звонить didMove(toParentViewController:_)программно, иначе они этого не сделают. По поводу сроков появления. методы, они вызываются в одинаковой последовательности в обоих направлениях. Что отличает, тем не менее, так это время viewDidLoad, потому что при встраивании он загружается раньше parent.viewDidLoad, а при программной, как мы и ожидали, это происходит во время parent.viewLoadLoad.
Роб
2
Я застрял на неработающих ограничениях; оказывается, я пропал translatesAutoresizingMaskIntoConstraints = false. Я не знаю, зачем это нужно или почему это помогает, но спасибо, что включили это в свой ответ.
hasen
1
@Rob На сайте developer.apple.com/library/archive/featuredarticles/… в листинге 5-1 есть строка кода Objective-C, которая говорит: «content.view.frame = [self frameForContentController];». Что такое frameForContentController в этом коде? Это рамка представления контейнера?
Дэниел Брауэр
24

@ Ответ Роба в 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

подробности

  • Xcode 10.2 (10E125), Swift 5

Решение

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

использование

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

Полный образец

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

Полученные результаты

введите описание изображения здесь введите описание изображения здесь введите описание изображения здесь

Василий Боднарчук
источник
1
Я использовал этот код , чтобы добавить tableViewControllerв viewControllerно не могу установить заголовок первых. Не знаю, возможно ли это. Я разместил этот вопрос . Как хорошо с вашей стороны, если вы посмотрите на это.
mahan
12

Вот мой код в 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)
}

}

использование

@IBOutlet weak var container:UIView!

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

Используйте другую функцию встраивания с контроллером представления без раскадровки.

Джеффри Чен
источник
2
Отличный класс, однако мне нужно встроить 2 viewController в один главный контроллер представления, что removeFromParentпредотвращает ваш вызов, как бы вы изменили свой класс, чтобы это разрешить?
GarySabo
блестяще :) Спасибо
Rebeloper
Это хороший пример, но как я могу добавить к нему некоторые анимации перехода (встраивание, замена дочерних контроллеров представления)?
Michał Ziobro