skip to Main Content

To design and create my UI, I always use auto layouts and do it programmatically instead of using storyboard.
In every view class of mine, I have a method called

private func setupView(frame:CGRect) {
      /* START CONTAINER VIEW */
    containerView = UIView()
    containerView.translatesAutoresizingMaskIntoConstraints = false
    addSubview(containerView)
    
    containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: frame.width * (13 / IPHONE8_SCREEN_WIDTH)).isActive = true
    containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -frame.width * (13 / IPHONE8_SCREEN_WIDTH)).isActive = true
    containerView.topAnchor.constraint(equalTo: topAnchor, constant: frame.height * (26 / IPHONE8_SCREEN_HEIGHT)).isActive = true
    containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    
    containerView.backgroundColor = UIColor.white
    /* END CONTAINER VIEW */
    ...
}

to initialize the components. Now let’s say, in the method above, I initialize 10 UI components which are properly displayed when I run my code. However, depending on some variables, I have another function that is being called

private func addNextRoundInformation() {
   ..
    nextRoundLabel = UILabel()
    nextRoundLabel.translatesAutoresizingMaskIntoConstraints = false
    containerView.addSubview(nextRoundLabel)
    
    nextRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
    nextRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
    nextRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
}

which should place a new label between some others which were already initialized.
Of course, when putting the new label between some particular ones, I also update the auto layout constraints of the of the bottom label like

 private func updateNumberOfWinnersLabelConstraint() {
    numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: nextRoundLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
    numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: nextRoundLabelValue.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
}

The topAnchor of each label depends on the bottom anchor of the previous one.

With this approach, I can’t see nextRoundLabel at all. It only appears, if I initialize it in the private func setupView(frame:CGRect) {}

Why?

2

Answers


  1. Chosen as BEST ANSWER

    I found a working solution:

    First, I declared two helper variables

    /* START HELPER VARIABLES */
    var numberOfWinnersLabelTopConstraint:NSLayoutConstraint?
    var numberOfWinnersLabelValueTopConstraint:NSLayoutConstraint?
    /* END HELPER VARIABLES */
    

    then I made some minor adjustments in my setupView function:

    private func setupView(frame:CGRect) {
       ...
         /* START NUMBER OF WINNERS PER ROUND LABEL */
        numberOfWinnersPerRoundLabel = UILabel()
        numberOfWinnersPerRoundLabel.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(numberOfWinnersPerRoundLabel)
        
        numberOfWinnersPerRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
        numberOfWinnersPerRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
       // numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true  // Replacing with helper variable
        
        numberOfWinnersLabelTopConstraint = numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
        numberOfWinnersLabelTopConstraint?.isActive = true
        numberOfWinnersPerRoundLabel.text = NSLocalizedString(NUMBER_OF_WINNERS_PER_ROUND_TEXT, comment: "")
        numberOfWinnersPerRoundLabel.textColor = .darkGray
        numberOfWinnersPerRoundLabel.adjustsFontSizeToFitWidth = true
        numberOfWinnersPerRoundLabel.font = .systemFont(ofSize: 12)
        /* END NUMBER OF WINNERS PER ROUND LABEL */
    
        /* START NUMBER OF WINNERS PER ROUND LABEL VALUE */
        numberOfWinnersPerRoundLabelValue = UILabel()
        numberOfWinnersPerRoundLabelValue.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(numberOfWinnersPerRoundLabelValue)
        
        numberOfWinnersPerRoundLabelValue.leadingAnchor.constraint(equalTo: currentRoundLabelValue.leadingAnchor).isActive = true
        numberOfWinnersPerRoundLabelValue.widthAnchor.constraint(equalTo:currentRoundLabelValue.widthAnchor).isActive = true
      //  numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: numberOfWinnersPerRoundLabel.topAnchor).isActive = true // replacing with helper variable
        
        numberOfWinnersLabelValueTopConstraint = numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: numberOfWinnersPerRoundLabel.topAnchor)
        numberOfWinnersLabelValueTopConstraint?.isActive = true
        
        numberOfWinnersPerRoundLabelValue.textColor = .black
        /* END NUMBER OF WINNERS PER ROUND LABEL VALUE */
    

    By introducing the helper variables, I could easily deactivate the topConstraint when adding the nextRoundLabel

     private func addNextRoundInformation() {
        nextRoundLabel = UILabel()
        nextRoundLabel.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(nextRoundLabel)
    
        nextRoundLabel.leadingAnchor.constraint(equalTo: currentRoundLabel.leadingAnchor).isActive = true
        nextRoundLabel.widthAnchor.constraint(equalTo:currentRoundLabel.widthAnchor).isActive = true
      
        nextRoundLabel.topAnchor.constraint(equalTo: roundEndsInLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT)).isActive = true
        nextRoundLabel.text = "Next round starts in"
        nextRoundLabel.textColor = .darkGray
        nextRoundLabel.font = .systemFont(ofSize: 12)
    
        nextRoundLabelValue = UILabel()
        nextRoundLabelValue.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(nextRoundLabelValue)
        
        nextRoundLabelValue.leadingAnchor.constraint(equalTo: currentRoundLabelValue.leadingAnchor).isActive = true
        nextRoundLabelValue.widthAnchor.constraint(equalTo:currentRoundLabelValue.widthAnchor).isActive = true
        nextRoundLabelValue.topAnchor.constraint(equalTo:nextRoundLabel.topAnchor).isActive = true
        nextRoundLabelValue.textColor = .black
        nextRoundLabelValue.text = "Next round label value"
        nextRoundLabelValue.font = .systemFont(ofSize: 14)
    }
    
    private func updateNumberOfWinnersLabelConstraint() {
        numberOfWinnersLabelTopConstraint?.isActive = false // Deactivate previous constraint
        numberOfWinnersLabelValueTopConstraint?.isActive = false
        
        numberOfWinnersLabelTopConstraint =  numberOfWinnersPerRoundLabel.topAnchor.constraint(equalTo: nextRoundLabel.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
        numberOfWinnersLabelTopConstraint?.isActive = true
        
        numberOfWinnersLabelValueTopConstraint = numberOfWinnersPerRoundLabelValue.topAnchor.constraint(equalTo: nextRoundLabelValue.bottomAnchor, constant: frame.height * (19 / IPHONE8_SCREEN_HEIGHT))
        numberOfWinnersLabelValueTopConstraint?.isActive = true
    }
    

    Basically, I only had to update the topConstraints of the numberOfWinnersPerRoundLabel and numberOfWinnersPerRoundLabelValue since everything else would be the same. No changes needed for currentRoundLabel.

    I tested it and it worked!


  2. You could do this with "Top-to-Bottom" constraints, but it would be rather complex.

    You would need to essentially create a "Linked List" to track each view, the views above and below it, and its constraints.

    So, to "insert" a new view after the 3rd view, you would need to:

    • deactivate the inter-view constraints
    • insert the new view into the linked list
    • re-create and activate the new constraints

    Putting your views in a UIStackView turns that process into a single line of code:

    stackView.insertArrangedSubview(newView, at: 3)
    

    Here’s a quick example:

    class ViewController: UIViewController {
        
        let testView: SampleView = SampleView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemYellow
            
            let infoLabel: UILabel = {
                let v = UILabel()
                v.textAlignment = .center
                v.numberOfLines = 0
                v.text = "Tap to Insert "New Label"nafter "Label 3""
                return v
            }()
            let btn: UIButton = {
                let v = UIButton()
                v.setTitle("Insert", for: [])
                v.setTitleColor(.white, for: .normal)
                v.setTitleColor(.lightGray, for: .highlighted)
                v.backgroundColor = .systemBlue
                return v
            }()
            
            [infoLabel, btn, testView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                infoLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
                infoLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                
                btn.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
                btn.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
                btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
    
                testView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 40.0),
                testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
                testView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
                
            ])
            
            btn.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
        }
        
        @objc func btnTapped(_ sender: Any?) {
            testView.insertNew()
        }
        
    }
    
    class SampleView: UIView {
        
        var containerView: UIView!
        var stackView: UIStackView!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            backgroundColor = .red
            setupView(frame: .zero)
        }
        
        private func setupView(frame:CGRect) {
            /* START CONTAINER VIEW */
            containerView = UIView()
            containerView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(containerView)
            
            stackView = UIStackView()
            stackView.axis = .vertical
            stackView.spacing = 8
            stackView.translatesAutoresizingMaskIntoConstraints = false
            containerView.addSubview(stackView)
            
            NSLayoutConstraint.activate([
    
                containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
                containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
                containerView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
                containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
                
                stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
                stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
                stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),
    
            ])
            
            containerView.backgroundColor = UIColor.white
            /* END CONTAINER VIEW */
            
            // add 10 labels to the stack view
            for i in 1...10 {
                let v = UILabel()
                v.text = "Label (i)"
                v.backgroundColor = .green
                stackView.addArrangedSubview(v)
            }
    
        }
        
        func insertNew() {
            let v = UILabel()
            v.text = "New Label"
            v.backgroundColor = .cyan
            v.translatesAutoresizingMaskIntoConstraints = false
            
            // we're adding a label after 3rd label
            stackView.insertArrangedSubview(v, at: 3)
        }
    }
    

    It starts looking like this:

    enter image description here

    after tapping the "Insert" button, it looks like this:

    enter image description here


    Edit

    To explain why your current approach isn’t working…

    Starting with this layout:

    enter image description here

    each label’s Top is constrained to the previous label’s Bottom (with constant: spacing).

    Those constraints are indicated by the blue arrows.

    You then want to "insert" Next Round Label between Round Ends In and Winners Per Round:

    enter image description here

    Your code:

    • adds the label
    • adds a constraint from the Top of Next Round Label to the Bottom of Round Ends In
    • adds a constraint from the Top of Winners Per Round to the Bottom of Next Round Label

    but… Winners Per Round already has a .topAnchor connected to Round Ends In, so it now has two top anchors.

    The conflicting constraints are shown in red:

    enter image description here

    As I said, I think your description of what you’re trying to do would lend itself to using stack views, and would make "inserting" views so much easier.

    But, if you need to stick with your current "Top-to-Bottom" constraints approach, you have several options.

    One – remove all the labels, then re-add and re-constrain them, including the one you’re inserting.

    Two – track the constraints (using an array or custom object properties) so you can deactivate the conflicting constraint(s).

    Three – use some code along the lines of

    let theConstraint = containerView.constraints.first(where: {($0.secondItem as? UILabel) == roundEndsInLabel})
    

    to "find" the constraint that needs to be deactivated.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search