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 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.