Reputation:
I’d like to create some reusable custom UI components to be used in my such as the following UIView
containing two labels. The labels may, depending on the content, be multi-line, and I provided some constraints for upper and lower margins. The views are added to my layout mainly inside UIStackView
s using Interface Builder or programmatically. Trouble here is, that the height of the views is not calculated correctly in runtime, cutting off a portion on the bottom of each view, particularly when there are multiple lines.
There is obviously some conceptual issue that I have not been able to figure out yet, and probably getting this two-label example right would help me getting a better understanding.
I commented out the overall height constraint, which I thought was necessary, but with that uncommented I see only the top line of the second label.
import UIKit
@IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 11.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 8.0
@IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
@IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
var viewHeight: CGFloat = 0.0
var firstLabel: UILabel!
var secondLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
func setUpView() {
firstLabel = UILabel()
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFontWeightBold)
firstLabel.numberOfLines = 3
firstLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
secondLabel = UILabel()
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFontWeightRegular)
secondLabel.numberOfLines = 20
secondLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
addSubview(firstLabel)
addSubview(secondLabel)
updateView()
}
func updateView() {
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
firstLabel.sizeToFit()
secondLabel.sizeToFit()
viewHeight = getHeight()
setNeedsUpdateConstraints()
}
override func updateConstraints() {
translatesAutoresizingMaskIntoConstraints = false
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
removeConstraints(constraints)
if self.isHidden == false {
self.addConstraint(NSLayoutConstraint(item: firstLabel, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: topMargin))
self.addConstraint(NSLayoutConstraint(item: firstLabel, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: firstLabel, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: secondLabel, attribute: .top, relatedBy: .equal, toItem: firstLabel, attribute: .bottom, multiplier: 1, constant: verticalSpacing))
self.addConstraint(NSLayoutConstraint(item: secondLabel, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: secondLabel, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0.0))
self.addConstraint(NSLayoutConstraint(item: secondLabel, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: bottomMargin))
//self.addConstraint(NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: viewHeight))
}
super.updateConstraints()
}
func getHeight() -> CGFloat {
return topMargin
+ firstLabel.frame.height
+ verticalSpacing
+ secondLabel.frame.height
+ bottomMargin
}
override open var intrinsicContentSize : CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: getHeight())
}
}
Upvotes: 0
Views: 1232
Reputation: 77432
Couple things...
First, you don't need to keep re-creating the constraints. Create them once when you setup the labels.
Second, you want to use the constraints to let auto-layout control the sizing - that's what they're for.
Third, auto-sizing multiline-labels can be tricky. Well, a better word might be confounding! For auto-layout to render and size the text in the label, it has to start with a width. Unfortunately, the common scenario is having the label's width controlled by something else - its superview, a stack view, etc. BUT... you also want the width of the label to control or "push out the sides" of its superview.
So, you need to make sure the label has a preferredMaxLayoutWidth
. Of course, you don't want to hard-code that - defeats the purpose of creating a flexible control.
The trick, from my experience anyway, is to force auto-layout to run a couple passes... and set the preferredMaxLayoutWidth
sorta "in the middle" of the process.
Try this out, and see if you get what you're going for:
//
// TwoLabelView.swift
//
// Created by Don Mag on 8/2/17.
//
class FixAutoLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
}
}
@IBDesignable class TwoLabelView: UIView {
var topMargin: CGFloat = 11.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 8.0
@IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
@IBInspectable var secondLabelText: String = "" { didSet { updateView() } }
var firstLabel: FixAutoLabel!
var secondLabel: FixAutoLabel!
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required public init?(coder: NSCoder) {
super.init(coder:coder)
setUpView()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setUpView()
}
func setUpView() {
firstLabel = FixAutoLabel()
firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFontWeightBold)
firstLabel.numberOfLines = 3
firstLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
secondLabel = FixAutoLabel()
secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFontWeightRegular)
secondLabel.numberOfLines = 20
secondLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail
addSubview(firstLabel)
addSubview(secondLabel)
// we're going to set the constraints
firstLabel .translatesAutoresizingMaskIntoConstraints = false
secondLabel.translatesAutoresizingMaskIntoConstraints = false
// pin both labels' left-edges to left-edge of self
firstLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
secondLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
// pin both labels' right-edges to right-edge of self
firstLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
secondLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
// pin firstLabel to the top of self + topMargin (padding)
firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin).isActive = true
// pin top of secondLabel to bottom of firstLabel + verticalSpacing
secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing).isActive = true
// pin bottom of self to bottom of secondLabel + bottomMargin (padding)
bottomAnchor.constraint(equalTo: secondLabel.bottomAnchor, constant: bottomMargin).isActive = true
// colors are just for debugging so we can see the frames of the labels
firstLabel.backgroundColor = .cyan
secondLabel.backgroundColor = .green
// call common "refresh" func
updateView()
}
func updateView() {
firstLabel.preferredMaxLayoutWidth = self.bounds.width
secondLabel.preferredMaxLayoutWidth = self.bounds.width
firstLabel.text = firstLabelText
secondLabel.text = secondLabelText
firstLabel.sizeToFit()
secondLabel.sizeToFit()
setNeedsUpdateConstraints()
}
override open var intrinsicContentSize : CGSize {
// just has to have SOME intrinsic content size defined
// this will be overridden by the constraints
return CGSize(width: 1, height: 1)
}
}
Upvotes: 1