プログラムでコンテナービューを追加する方法


107

コンテナービューは、インターフェイスエディターを介してストーリーボードに簡単に追加できます。追加されると、コンテナービューはプレースホルダービュー、埋め込みセグエ、および(子)ビューコントローラーになります。

しかし、プログラムでコンテナビューを追加する方法を見つけることができません。実際、UIContainerViewそのようなクラスを見つけることすらできません。

Container Viewのクラスの名前は、きっと良いスタートです。セグエを含む完全なガイドは大歓迎です。

View Controllerプログラミングガイドを知っていますが、Interface BuilderがContainer Viewerに対して行う方法と同じではありません。たとえば、制約が適切に設定されている場合、(子)ビューはコンテナビューのサイズ変更に適応します。


1
「制約が適切に設定されている場合、(子)ビューはコンテナビューのサイズ変更に適応します」とはどういう意味ですか?制約は、IBのコンテナービューで実行した場合でも、プログラムでビューコントローラーコンテインメントで実行した場合でも同じように機能します。
Rob

1
最も重要なことは、組み込みViewControllerのライフサイクルです。ViewControllerInterface Builderによる組み込みのライフサイクルは正常ですが、プログラムで追加されたものviewDidAppearにも、どちらにviewWillAppear(_:)もありませんviewWillDisappear
DawnSong 2017

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

この後者のパターンは、異なる子ビューコントローラー間で遷移し、1つの子のビューが前の子のビューと同じ場所にあることを確認したい場合に非常に役立ちます(つまり、配置のすべての一意の制約はコンテナービューによって決まります)。毎回これらの制約を再構築する必要はありません)。ただし、単純なビューの封じ込めを実行するだけの場合、この個別のコンテナビューの必要性はそれほど魅力的ではありません。


上記の例では、自分で制約translatesAutosizingMaskIntoConstraintsfalse定義するように設定しています。あなたは明らかに残すことができますtranslatesAutosizingMaskIntoConstraintsようtrue、両方の設定frameautosizingMaskあなたが好む場合は、追加のビューのために。


Swift 3およびSwift 2レンディションについては、この回答の以前のリビジョンを参照してください。


あなたの答えは完全ではないと思います。最も重要なことは、組み込みViewControllerのライフサイクルです。ViewControllerInterface Builderによる組み込みのライフサイクルは正常ですが、プログラムで追加されたものviewDidAppearにも、どちらにviewWillAppear(_:)もありませんviewWillDisappear
DawnSong 2017

もう一つ奇妙なこと埋め込まれたということであるViewControllerのは、viewDidAppearその親の中で呼び出されたviewDidLoad代わりに、その親の間に、viewDidAppear
DawnSong

@DawnSong-「しかし、プログラムで追加されたものにはviewDidAppear、[も]も、どちらviewWillAppear(_:)もありませんviewWillDisappear」。will方法は、両方のシナリオで正しく呼ばれて表示されます。didMove(toParentViewController:_)プログラムでそれを行う場合は、呼び出す必要があります。それ以外の場合は呼び出しません。登場のタイミングについて。メソッドは、同じ順序で双方向に呼び出されます。違いは、thoはのタイミングですviewDidLoad。なぜなら、embedの場合、それはの前parent.viewDidLoadにロードされますが、プログラマティックの場合、予想通り、の間に発生しparent.viewLoadLoadます。
Rob

2
制約が機能しないことに悩まされました。私は行方不明だったことがわかりtranslatesAutoresizingMaskIntoConstraints = falseます。なぜそれが必要なのか、なぜうまくいくのかはわかりませんが、答えに含めてくれてありがとう。
2018

1
で@Rob developer.apple.com/library/archive/featuredarticles/...リスト5-1には、言うObjective-Cのコードの行がある"content.view.frame = [自己frameForContentController];"。そのコードの「frameForContentController」とは何ですか?それはコンテナビューのフレームですか?
Daniel Brower

24

Swift 3での@Robの回答:

    // 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
素晴らしいクラスですが、同じマスターView Controller内に2つのviewControllerを埋め込む必要があることに気づきました。これremoveFromParentにより、呼び出しが妨げられます。これを許可するようにクラスをどのように修正しますか?
GarySabo 2018年

素晴らしい:)ありがとう
Rebeloper 2018年

良い例ですが、トランジションアニメーションをこれに追加するにはどうすればよいですか(子ビューコントローラーの埋め込み、置き換え)。
のMichałZiobro
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.