哪个生命周期事件用于在子视图控制器中声明布局约束?

Which lifecycle event to use for declaring layout constraints in a child view controller?

我有一个简单的父视图控制器,我正在向它添加一个子视图控制器。这是父级:

class ParentViewController: UIViewController {

    private var childViewController: ChildViewController!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        childViewController = ChildViewController()
        addChild(childViewController)
        view.addSubview(childViewController.view)
        childViewController.didMove(toParent: self)
        
        NSLayoutConstraint.activate([
            childViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
            childViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 50),
            childViewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -50),
            childViewController.view.heightAnchor.constraint(equalToConstant: 100)
        ])
    }
}

子视图控制器声明如下:

class ChildViewController: UIViewController {

    private let label1: UILabel = {
        let label = UILabel()
        label.text = "First label"
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private let label2: UILabel = {
        let label = UILabel()
        label.text = "Second label"
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemYellow
        view.translatesAutoresizingMaskIntoConstraints = false
        setupSubViews()
    }
    
    private func setupSubViews() {
        view.addSubview(label1)
        view.addSubview(label2)
    
        print("view.frame.size: \(view.frame.size)")

        NSLayoutConstraint.activate([
            label1.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
            label1.centerYAnchor.constraint(equalTo: view.topAnchor, constant: self.view.frame.size.height * (1/3)),
            label2.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
            label2.centerYAnchor.constraint(equalTo: view.topAnchor, constant: self.view.frame.size.height * (2/3)),
        ])
    }
}

运行 代码生成以下内容:

这两个标签的位置显然不是我想要的。我在子视图控制器中尝试做的是使用子视图控制器的视图在父视图控制器中定位后的高度为两个标签定义 centerYAnchor 约束。如果我在我的子视图控制器中打印出 setupSubViews() 内部 view.frame.size 的值,则视图的大小就是整个屏幕(在我的例子中是 428.0 x 926.0)。据推测,这是因为子视图控制器的视图尚未完全loaded/positioned在父视图控制器视图中?

所以我将对 setupSubViews() 的调用移动到子视图控制器的 viewDidLayoutSubviews() 中,然后 view.frame.size 的值是正确的 (328.0 x 100.0) 并且标签位置正确在子视图控制器的视图中。但是我确实看到 viewDidLayoutSubviews() 被多次调用,所以我想知道这是否真的是声明这样的约束的“正确”生命周期方法?我看到有人建议使用布尔值来确保约束代码只运行一次,但我不确定这是否是处理这种情况的正确方法。

只要视图的高度发生变化,您以这种方式创建的约束就必须更新,这可能会发生多次。发生这种情况时,将调用 viewDidLayoutSubviews。所以将更新约束常量的代码放在viewDidLayoutSubviews中并没有不合适,只是因为它被调用了多次。您不想仅在 第一次 视图高度更改时设置约束常量,对吗?

但是请注意,您应该只更新 viewDidLayoutSubviews 中的约束常量,而不是在那里调用 setupSubview。您应该在 loadView 中调用 setupSubview,而不是 viewDidLoad,因为您是以编程方式构建视图。参见 What is the difference between loadView and viewDidLoad?

事实上,这样做的效果要好得多。您可以使用乘数将标签的中心 Y 约束到视图的中心 Y,而不是使约束保持不变。标签 1 的倍数为 2/3,标签 2 的倍数为 4/3。这将使它们分别位于视图顶部的三分之一和三分之二处,

NSLayoutConstraint(
    item: label1, 
    attribute: .centerY, 
    relatedBy: .equal, 
    toItem: view, 
    attribute: .centerY, 
    multiplier: 2/3, 
    constant: 0),
NSLayoutConstraint(
    item: label2, 
    attribute: .centerY, 
    relatedBy: .equal, 
    toItem: view, 
    attribute: .centerY, 
    multiplier: 4/3, 
    constant: 0),