Search code examples
iosiphoneswiftuiviewnslayoutconstraint

Having problems with NSLayoutConstraints and using Auto Layout programmatically, UIViews stuck at (0,0)


I'm trying to programmatically generate a 'score page' where by I have a UILabel and a UISlider for each attribute's score. Since there isn't a fixed number of attributes, I've decided to do this programmatically (as opposed to in story board)

My idea of going about doing this was to create a UIView for each attribute and then insert one UILabel and one UISlider into the UIView, and then setting up constraints after.

However, I'm running into a problem whereby I'm unable to set up the constraints properly, or another huge error that I might have missed out due to inexperience in doing such things. As a result, all the UIViews are stuck to the top left of the screen (0,0) and are on top of one another.

Here's my code so far :

func addLabels(attributesArray: [String], testResultsDictionary: [String:Double]){
    var viewsDictionary = [String:AnyObject]()
    //let numberOfAttributes = attributesArray.count //(vestigial, please ignore)
    let sliderHeight = Double(20)
    let sliderWidth = Double(UIScreen.mainScreen().bounds.size.width)*0.70 // 70% across screen
    let labelToSliderDistance = Float(10)
    let sliderToNextLabelDistance = Float(30)
    let edgeHeightConstraint = Float(UIScreen.mainScreen().bounds.size.height)*0.10 // 10% of screen height

    for attribute in attributesArray {

        let attributeView = UIView(frame: UIScreen.mainScreen().bounds)
        attributeView.backgroundColor = UIColor.blueColor()
        attributeView.translatesAutoresizingMaskIntoConstraints = false
        attributeView.frame.size = CGSize(width: Double(UIScreen.mainScreen().bounds.size.width)*0.80, height: Double(80))
        self.view.addSubview(attributeView)

        var attributeViewsDictionary = [String:AnyObject]()
        let attributeIndex = attributesArray.indexOf(attribute)! as Int

        let attributeLabel = UILabel()
        attributeLabel.translatesAutoresizingMaskIntoConstraints = false
        attributeLabel.text = attribute.stringByReplacingOccurrencesOfString("_", withString: " ")
        attributeLabel.sizeToFit()

        let attributeSlider = UISlider()
        attributeSlider.translatesAutoresizingMaskIntoConstraints = false
        attributeSlider.setThumbImage(UIImage(), forState: .Normal)
        attributeSlider.frame.size = CGSize(width: sliderWidth, height: sliderHeight)
        attributeSlider.userInteractionEnabled = false

        if let sliderValue = testResultsDictionary[attribute] {
            attributeSlider.value = Float(sliderValue)
        }
        else {
            attributeSlider.value = 0
        }

        attributeView.addSubview(attributeLabel)
        attributeView.addSubview(attributeSlider)

        //attributeView.sizeToFit()

        attributeViewsDictionary["Label"] = attributeLabel
        attributeViewsDictionary["Slider"] = attributeSlider

        viewsDictionary[attribute] = attributeView
        print(viewsDictionary)

        let control_constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:|-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
        var control_constraint_V = [NSLayoutConstraint]()
        if attributeIndex == 0 {
            control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:|-\(edgeHeightConstraint)-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
        }
        else if attributeIndex == attributesArray.indexOf(attributesArray.last!){
            control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[\(attribute)]-\(edgeHeightConstraint)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
        }
        else {
            control_constraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[\(attributesArray[attributeIndex-1])]-\(sliderToNextLabelDistance)-[\(attribute)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
        }

        self.view.addConstraints(control_constraint_H)
        self.view.addConstraints(control_constraint_V)

        let interAttributeConstraint_V = NSLayoutConstraint.constraintsWithVisualFormat("V:[Label]-\(labelToSliderDistance)-[Slider]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: attributeViewsDictionary)
        //let interAttributeConstraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:[Label]-5-[Slider]", options: NSLayoutFormatOptions.AlignAllCenterX, metrics: nil, views: attributeViewsDictionary)

        attributeView.addConstraints(interAttributeConstraint_V)
        //attributeView.addConstraints(interAttributeConstraint_H)
        //attributeView.sizeToFit()
    }
}

Extra Notes: - An attributeArray looks something like this: ["Happiness", "Creativity", "Tendency_To_Slip"]

  • The code is extremely messy and unnecessarily long as it is a prototype, so sorry! Please bear with it!

Solution

  • The issue is that these views do not have their constraints fully defined (notably, there were a lot of missing vertical constraints). I also note that you've attempted to set the size of the frame of various views, but that is for naught because when you use auto layout, all frame values will be discarded and recalculated by the auto layout process. Instead, make sure the views dimensions are fully defined entirely by the constraints.

    For example:

    let spacing: CGFloat = 10
    
    func addLabels(attributesArray: [String], testResultsDictionary: [String: Float]) {
        var previousContainer: UIView?   // rather than using the index to look up the view for the previous container, just have a variable to keep track of the previous one for you.
    
        for attribute in attributesArray {
            let container = UIView()
            container.backgroundColor = UIColor.lightGrayColor() // just so I can see it
            container.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(container)
    
            // set top anchor for container to superview if no previous container, otherwise link it to the previous container
    
            if previousContainer == nil {
                container.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: spacing).active = true
            } else {
                container.topAnchor.constraintEqualToAnchor(previousContainer!.bottomAnchor, constant: spacing).active = true
            }
            previousContainer = container
    
            // set leading/trailing constraints for container to superview
    
            NSLayoutConstraint.activateConstraints([
                container.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor, constant: spacing),
                view.trailingAnchor.constraintEqualToAnchor(container.trailingAnchor, constant: spacing),
            ])
    
            // create label
    
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            label.text = attribute
            container.addSubview(label)
    
            // create slider
    
            let slider = UISlider()
            slider.translatesAutoresizingMaskIntoConstraints = false
            slider.value = testResultsDictionary[attribute]!
            container.addSubview(slider)
    
            // constraints for label and slider
    
            NSLayoutConstraint.activateConstraints([
                label.topAnchor.constraintEqualToAnchor(container.topAnchor, constant: spacing),
                slider.topAnchor.constraintEqualToAnchor(label.bottomAnchor, constant: spacing),
                container.bottomAnchor.constraintEqualToAnchor(slider.bottomAnchor, constant: spacing),
    
                label.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: spacing),
                slider.leadingAnchor.constraintEqualToAnchor(container.leadingAnchor, constant: spacing),
    
                container.trailingAnchor.constraintEqualToAnchor(label.trailingAnchor, constant: spacing),
                container.trailingAnchor.constraintEqualToAnchor(slider.trailingAnchor, constant: spacing)
            ])
        }
    }
    

    Now, I happen to be using the iOS 9 syntax for defining constraints (it is expressive and concise), but if you want/need to use VFL you can do that, too. Just make sure that you define an equivalent set of constraints which are unambiguously defined (top, bottom, leading and trailing). Also note that rather than hardcoding the size of these container views, I let it infer it from the size of its subviews and the container views will resize accordingly.

    Having said all of this, I look at this UI and I might be inclined to do this with a table view, which gets you out of the business of having to define all of these constraints, but also gracefully handles the scenario where there are so many of these that you want to enjoy scrolling behavior, too. Or, if I knew that these were always going to be able to fit on a single screen, I might use a UIStackView. But if you want to do it with constraints, you might do something like above.