Cara menambahkan Tampilan Penampung secara terprogram

107

Tampilan Penampung dapat dengan mudah ditambahkan ke papan cerita melalui Editor Antarmuka. Saat ditambahkan, Tampilan Penampung adalah tampilan tempat penampung, segmen sematan, dan pengontrol tampilan (anak).

Namun, saya tidak dapat menemukan cara untuk menambahkan Tampilan Penampung secara terprogram. Sebenarnya, saya bahkan tidak dapat menemukan kelas bernama UIContainerViewatau lebih.

Nama untuk kelas Tampilan Penampung jelas merupakan awal yang baik. Panduan lengkap termasuk segue akan sangat dihargai.

Saya mengetahui Panduan Pemrograman Pengontrol Tampilan, tetapi saya tidak menganggapnya sama seperti yang dilakukan Pembuat Antarmuka untuk Penampil Penampung. Misalnya, ketika batasan disetel dengan benar, tampilan (anak) akan menyesuaikan dengan perubahan ukuran dalam Tampilan Penampung.

Dante May Code
sumber
1
Apa yang Anda maksud saat Anda mengatakan "ketika batasan disetel dengan benar, tampilan (anak) akan menyesuaikan dengan perubahan ukuran dalam Tampilan Penampung" (sehingga menyiratkan bahwa ini tidak benar saat Anda melakukan penahanan pengontrol tampilan)? Batasan berfungsi sama baik Anda melakukannya melalui tampilan penampung di IB atau melihat penahanan pengontrol secara terprogram.
Rob
1
Yang paling penting adalah ViewControllersiklus hidup tertanam . ViewControllerSiklus hidup yang disematkan oleh Interface Builder adalah normal, tetapi yang ditambahkan secara terprogram memiliki viewDidAppear, tidak viewWillAppear(_:)juga viewWillDisappear.
DawnSong
2
@DawnSong - Jika Anda melakukan panggilan penahanan tampilan dengan benar, viewWillAppeardan viewWillDisappeardipanggil pada pengontrol tampilan anak, baik-baik saja. Jika Anda memiliki contoh di mana mereka tidak, Anda harus mengklarifikasi, atau memposting pertanyaan Anda sendiri menanyakan mengapa mereka tidak.
Rob

Jawaban:

228

Sebuah "tampilan kontainer" storyboard hanyalah UIViewobjek standar . Tidak ada jenis "tampilan penampung" khusus. Faktanya, jika Anda melihat hierarki tampilan, Anda dapat melihat bahwa "tampilan penampung" adalah standar UIView:

tampilan kontainer

Untuk mencapai ini secara terprogram, Anda menggunakan "penahanan pengontrol tampilan":

  • Buat instantiateViewController(withIdentifier:)instance pengontrol tampilan anak dengan memanggil objek storyboard.
  • Panggil addChildpengontrol tampilan orang tua Anda.
  • Tambahkan pengontrol tampilan viewke hierarki tampilan Anda dengan addSubview(dan juga setel framebatasan atau yang sesuai).
  • Panggil didMove(toParent:)metode pada pengontrol tampilan anak, dengan meneruskan referensi ke pengontrol tampilan induk.

Lihat Menerapkan Pengontrol Tampilan Penampung di Panduan Pemrograman Pengontrol Tampilan dan bagian "Menerapkan Pengontrol Tampilan Penampung" dari Referensi Kelas UIViewController .


Misalnya, di Swift 4.2 akan terlihat seperti ini:

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

Perhatikan, hal di atas sebenarnya tidak menambahkan "tampilan container" ke hierarki. Jika Anda ingin melakukan itu, Anda akan melakukan sesuatu seperti:

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

Pola terakhir ini sangat berguna jika pernah bertransisi antara pengontrol tampilan anak yang berbeda dan Anda hanya ingin memastikan tampilan satu anak berada di lokasi yang sama dan tampilan anak sebelumnya (yaitu semua batasan unik untuk penempatan ditentukan oleh tampilan penampung, daripada perlu membangun kembali batasan ini setiap saat). Namun jika hanya melakukan penahanan tampilan sederhana, kebutuhan akan tampilan penampung terpisah ini kurang menarik.


Dalam contoh di atas, saya menetapkan sendiri translatesAutosizingMaskIntoConstraintsuntuk falsemenentukan batasan. Anda jelas dapat meninggalkan translatesAutosizingMaskIntoConstraintssebagai truedan menyetel framedan autosizingMaskuntuk tampilan yang Anda tambahkan, jika Anda mau.


Lihat revisi sebelumnya dari jawaban ini untuk membawakan Swift 3 dan Swift 2 .

rampok
sumber
Saya rasa jawaban Anda tidak lengkap. Yang paling penting adalah ViewControllersiklus hidup tertanam . ViewControllerSiklus hidup yang disematkan oleh Interface Builder adalah normal, tetapi yang ditambahkan secara terprogram memiliki viewDidAppear, tidak viewWillAppear(_:)juga viewWillDisappear.
DawnSong
Hal aneh lainnya adalah bahwa embedded ViewController's viewDidAppeardisebut dalam induknya viewDidLoad, bukan pada induknyaviewDidAppear
DawnSong
@DawnSong - "tetapi yang ditambahkan secara terprogram memiliki viewDidAppear, [tetapi] tidak viewWillAppear(_:)juga viewWillDisappear". Metode willmuncul dipanggil dengan benar di kedua skenario. Seseorang harus menelepon didMove(toParentViewController:_)ketika melakukannya secara terprogram, atau mereka tidak akan melakukannya. Mengenai waktu kemunculannya. metode, mereka dipanggil dalam urutan yang sama dua arah. Yang berbeda, adalah waktu viewDidLoad, karena dengan embed, itu dimuat sebelumnya parent.viewDidLoad, tetapi dengan terprogram, seperti yang kita harapkan, itu terjadi selama parent.viewLoadLoad.
Rob
2
Saya terjebak pada kendala tidak bekerja; ternyata saya hilang translatesAutoresizingMaskIntoConstraints = false. Saya tidak tahu mengapa itu diperlukan atau mengapa itu membuat semuanya berfungsi, tetapi terima kasih telah memasukkannya dalam jawaban Anda.
hasen
1
@Rob Di developer.apple.com/library/archive/featuredarticles/… di Listing 5-1, ada baris kode Objective-C yang mengatakan, "content.view.frame = [self frameForContentController];". Apa itu "frameForContentController" dalam kode itu? Apakah itu bingkai tampilan wadah?
Daniel Brower
24

@ Jawaban Rob di 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)
Masa depan yang cerah
sumber
13

Detail

  • Xcode 10.2 (10E125), Swift 5

Larutan

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

Pemakaian

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

Sampel lengkap

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

Hasil

masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini masukkan deskripsi gambar di sini

Bodnarchuk dengan mudah
sumber
1
Saya telah menggunakan kode ini untuk menambahkan tableViewControllerdalam viewControllertetapi tidak bisa mengatur judul mantan. Saya tidak tahu apakah mungkin untuk melakukannya. Saya telah memposting pertanyaan ini . Anda baik jika melihatnya.
mahan
12

Ini kode saya di 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)
}

}

Pemakaian

@IBOutlet weak var container:UIView!

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

Gunakan fungsi sematan lainnya dengan pengontrol tampilan non-storyboard.

Jeffrey Chen
sumber
2
Kelas hebat, namun saya merasa perlu menyematkan 2 viewControllers dalam pengontrol tampilan master yang sama, yang removeFromParentmencegah panggilan Anda , bagaimana Anda akan mengubah kelas Anda untuk mengizinkan ini?
GarySabo
brilian :) Terima kasih
Rebeloper
Ini adalah contoh yang bagus, tetapi bagaimana saya dapat menambahkan beberapa animasi transisi ke dalamnya (menyematkan, mengganti pengontrol tampilan anak)?
Michał Ziobro