Як додати Container View програмно


107

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

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

Назва класу Container View - це, безумовно, хороший початок. Повне керівництво, включаючи segue, буде дуже вдячно.

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


1
Що ви маєте на увазі, коли ви говорите "коли обмеження встановлені належним чином, подання (дочірня) буде адаптуватися до змін розміру в режимі перегляду контейнерів" (тим самим маючи на увазі, що це не відповідає дійсності, якщо ви утримуєте контролер перегляду)? Обмеження працюють так само, як ви це робили за допомогою перегляду контейнерів у IB або програмного забезпечення контролера перегляду.
Роб

1
Найголовніше - це вбудований ViewControllerжиттєвий цикл. Вбудований ViewControllerжиттєвий цикл Interface Builder є нормальним, але доданий програмно не має viewDidAppear, ні, viewWillAppear(_:)ні viewWillDisappear.
Світанок

2
@DawnSong - Якщо ви робите вигляд стримування викликів правильно, viewWillAppearі viewWillDisappearназиваються на контролері уявлення дитини, просто відмінно. Якщо у вас є приклад, де їх немає, слід уточнити або опублікувати власне запитання, запитавши, чому це не так.
Роб

Відповіді:


228

"Перегляд контейнерів" для розкадровки - це лише стандартний 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.
Світанок

Ще одна дивна річ у тому, що вбудований ViewController's 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

Ось мій код у швидкій 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 viewControllers в один контролер головного перегляду, який removeFromParentзабороняє ваш виклик, як би ви змінили свій клас, щоб дозволити це?
GarySabo

блискучий :) Дякую
Ребелопер

Це приємний приклад, але як я можу додати до цього анімацію переходу (вбудовування, заміна контролерів дочірнього перегляду)?
Michał Ziobro
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.