Cách thêm Chế độ xem vùng chứa theo chương trình


107

Có thể dễ dàng thêm Chế độ xem vùng chứa vào bảng phân cảnh thông qua Trình chỉnh sửa giao diện. Khi được thêm vào, Chế độ xem vùng chứa là một chế độ xem trình giữ chỗ, một phân đoạn nhúng và một bộ điều khiển chế độ xem (con).

Tuy nhiên, tôi không thể tìm cách thêm Chế độ xem vùng chứa theo chương trình. Trên thực tế, tôi thậm chí không thể tìm thấy một lớp học có tên UIContainerViewnhư vậy.

Một cái tên cho lớp Chế độ xem vùng chứa chắc chắn là một khởi đầu tốt. Một hướng dẫn đầy đủ bao gồm cả segue sẽ được đánh giá cao.

Tôi biết Hướng dẫn lập trình bộ điều khiển chế độ xem, nhưng tôi không coi nó giống như cách Trình tạo giao diện thực hiện cho Trình xem vùng chứa. Ví dụ: khi các ràng buộc được đặt đúng cách, dạng xem (con) sẽ thích ứng với các thay đổi về kích thước trong Dạng xem vùng chứa.


1
Ý bạn là gì khi bạn nói "khi các ràng buộc được đặt đúng cách, chế độ xem (con) sẽ thích ứng với các thay đổi về kích thước trong Chế độ xem vùng chứa" (do đó ngụ ý rằng điều này không đúng khi bạn thực hiện việc chứa bộ điều khiển chế độ xem)? Các ràng buộc hoạt động giống nhau cho dù bạn đã làm điều đó qua chế độ xem vùng chứa trong IB hay chế độ xem bộ điều khiển ngăn theo lập trình.
Rob

1
Điều quan trọng nhất là ViewControllervòng đời của nhúng . Vòng đời được nhúng ViewControllerbởi Interface Builder là bình thường, nhưng vòng đời được thêm vào theo chương trình thì có viewDidAppear, viewWillAppear(_:)cũng không viewWillDisappear.
DawnSong

2
@DawnSong - Nếu bạn thực hiện lệnh ngăn chế độ xem một cách chính xác, viewWillAppearviewWillDisappearđược gọi trên bộ điều khiển chế độ xem con, tốt thôi. Nếu bạn có một ví dụ mà họ không, bạn nên làm rõ hoặc đăng câu hỏi của riêng bạn để hỏi tại sao họ không.
Rob

Câu trả lời:


228

"Chế độ xem vùng chứa" trong bảng phân cảnh chỉ là một UIViewđối tượng tiêu chuẩn . Không có kiểu "chế độ xem vùng chứa" đặc biệt. Trên thực tế, nếu bạn nhìn vào phân cấp chế độ xem, bạn có thể thấy rằng "chế độ xem vùng chứa" là một tiêu chuẩn UIView:

xem container

Để đạt được điều này theo chương trình, bạn sử dụng "chức năng ngăn bộ điều khiển chế độ xem":

  • Khởi tạo bộ điều khiển chế độ xem trẻ em bằng cách gọi instantiateViewController(withIdentifier:) đối tượng bảng phân cảnh.
  • Gọi addChild trong bộ điều khiển chế độ xem cha mẹ của bạn.
  • Thêm bộ điều khiển viewchế độ xem vào hệ thống phân cấp chế độ xem của bạn với addSubview(và cũng đặtframe hoặc các ràng buộc nếu thích hợp).
  • Gọi didMove(toParent:)phương thức trên bộ điều khiển chế độ xem con, chuyển tham chiếu đến bộ điều khiển chế độ xem mẹ.

Xem Triển khai bộ điều khiển dạng xem vùng chứa trong Hướng dẫn lập trình bộ điều khiển dạng xem và phần "Triển khai bộ điều khiển dạng xem vùng chứa" của Tham chiếu lớp UIViewController .


Ví dụ, trong Swift 4.2, nó có thể giống như sau:

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

Lưu ý, phần trên không thực sự thêm "chế độ xem vùng chứa" vào hệ thống phân cấp. Nếu bạn muốn làm điều đó, bạn phải làm điều gì đó như:

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

Mẫu thứ hai này cực kỳ hữu ích nếu bạn từng chuyển đổi giữa các bộ điều khiển chế độ xem trẻ em khác nhau và bạn chỉ muốn đảm bảo chế độ xem của một đứa trẻ ở cùng một vị trí và chế độ xem của đứa trẻ trước đó (nghĩa là tất cả các ràng buộc duy nhất cho vị trí được quy định bởi chế độ xem vùng chứa, thay vì cần phải xây dựng lại các ràng buộc này mỗi lần). Nhưng nếu chỉ thực hiện ngăn chế độ xem đơn giản, nhu cầu về chế độ xem vùng chứa riêng biệt này sẽ kém hấp dẫn hơn.


Trong các ví dụ trên, tôi đang thiết lập translatesAutosizingMaskIntoConstraintsđể tự falsexác định các ràng buộc. Bạn rõ ràng là có thể để lại translatesAutosizingMaskIntoConstraintsnhư truevà đặt cả frameautosizingMaskcho quan điểm bạn thêm, nếu bạn muốn.


Xem các bản sửa đổi trước của câu trả lời này cho các phiên bản Swift 3Swift 2 .


Tôi không nghĩ rằng câu trả lời của bạn là hoàn chỉnh. Điều quan trọng nhất là ViewControllervòng đời của nhúng . Vòng đời được nhúng ViewControllerbởi Interface Builder là bình thường, nhưng vòng đời được thêm vào theo chương trình thì có viewDidAppear, viewWillAppear(_:)cũng không viewWillDisappear.
DawnSong

Một điều lạ là nhúng ViewController's viewDidAppearđược gọi là ở cha mẹ của mình viewDidLoad, thay vì trong suốt của mẹviewDidAppear
DawnSong

@DawnSong - "nhưng cái được thêm vào theo chương trình thì có viewDidAppear, [nhưng] viewWillAppear(_:)cũng không viewWillDisappear". Các willphương thức xuất hiện được gọi chính xác trong cả hai trường hợp. Người ta phải gọi didMove(toParentViewController:_)khi thực hiện nó theo chương trình, tho, nếu không thì họ sẽ không. Về thời điểm xuất hiện. chúng được gọi theo cùng một trình tự theo cả hai cách. Điều khác biệt, tho, là thời gian viewDidLoad, bởi vì với nhúng, nó được tải trước đó parent.viewDidLoad, nhưng với có lập trình, như chúng ta mong đợi, nó xảy ra trong suốt thời gian đó parent.viewLoadLoad.
Rob

2
Tôi bị mắc kẹt vào những ràng buộc không hoạt động; hóa ra tôi đã mất tích translatesAutoresizingMaskIntoConstraints = false. Tôi không biết tại sao nó cần hoặc tại sao nó làm cho mọi thứ hoạt động, nhưng cảm ơn bạn đã đưa nó vào câu trả lời của mình.
hasen

1
@Rob Tại developer.apple.com/library/archive/featuredarticles/… trong Liệt kê 5-1, có một dòng mã Objective-C cho biết, "content.view.frame = [self frameForContentController];". "FrameForContentController" trong mã đó là gì? Đó có phải là khung của khung nhìn vùng chứa không?
Daniel Brower

24

Câu trả lời của @ Rob trong 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

Chi tiết

  • Xcode 10.2 (10E125), Swift 5

Giải pháp

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

Sử dụng

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

Đầy đủ mẫu

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

Các kết quả

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


1
Tôi đã sử dụng mã này để thêm tableViewControllervào viewControllernhưng không thể đặt tiêu đề của cái trước. Tôi không biết nếu nó có thể làm như vậy. Tôi đã đăng câu hỏi này . Thật tốt cho bạn nếu bạn được nhìn vào nó.
mahan

12

Đây là mã của tôi trong 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)
}

}

Sử dụng

@IBOutlet weak var container:UIView!

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

Sử dụng chức năng nhúng khác với bộ điều khiển chế độ xem không phải bảng phân cảnh.


2
Lớp học tuyệt vời, tuy nhiên, tôi thấy mình cần phải nhúng 2 viewControllers trong cùng một bộ điều khiển chế độ xem chính, điều mà removeFromParentlệnh gọi của bạn ngăn cản, bạn sẽ sửa đổi lớp của mình như thế nào để cho phép điều này?
GarySabo

tuyệt vời :) Cảm ơn bạn
Rebeloper

Đây là một ví dụ hay, nhưng làm cách nào để thêm một số hoạt ảnh chuyển tiếp vào điều này (nhúng, thay thế bộ điều khiển chế độ xem con)?
Michał Ziobro
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.