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

Time:03-04

I have a simple parent view controller and I'm adding a child view controller to it. Here's the parent:

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

The child view controller is declared like this:

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

Running the code produces the following:

enter image description here

The position of the two labels are obviously not what I intended. What I'm trying to do in the child view controller is define centerYAnchor constraints for the two labels using the height of the child view controller's view after it has been positioned in the parent view controller. If I print out the value of view.frame.size inside of setupSubViews() in my child view controller, the size of the view is the entire screen (428.0 x 926.0, in my case). Presumably, this is because the child view controller's view hasn't been fully loaded/positioned in the parent view controllers view yet?

So I moved the call to setupSubViews() into viewDidLayoutSubviews() of the child view controller and then the value of view.frame.size is correct (328.0 x 100.0) and the labels are positioned correctly within the child view controller's view. But I do see viewDidLayoutSubviews() being called multiple times, though, so I'm wondering if that's really the "correct" lifecycle method for declaring constraints like this? I've seen some people suggest using a boolean to ensure that the constraint code only runs once but I'm not sure that's the right way to handle this situation either.

CodePudding user response:

The constraint you create this way must be updated whenever the height of the view changes, which can happen multiple times. When this happens, viewDidLayoutSubviews gets called. So it is not inappropriate to put the code to update the constraints' constants in viewDidLayoutSubviews, just because it is called multiple times. You don't want to set the constraints' constants only on the first time the height of the view changes, right?

Though, note that you should just update the constraint's constants in viewDidLayoutSubviews, and not call setupSubview there. You should call setupSubview in loadView, not viewDidLoad, since you are building the view programmatically. See What is the difference between loadView and viewDidLoad?

In fact, there is a much better of doing this. Rather than making the constraints constant, you can constraint the label's center Y to the view's center Y with a multiplier. Label 1 will have a multiplier of 2/3, and label 2 will have 4/3. This will make them one third, and two thirds of the way from the top of the view respectively,

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),
  • Related