Labels in iOS are create like (1), no horizontal margin and no beauty at all. I would like to create a label like in (2), curved edges and a margin left and right
The contents of this label is updated 2 times per second and its width must change dynamically.
So I have created this class
@IBDesignable
class BeautifulLabel : UILabel {
// private var internalRect : CGRect? = .zero
override func drawText(in rect: CGRect) {
let insets = UIEdgeInsets(top: marginTop,
left: marginLeft,
bottom: marginBottom,
right: marginRight)
super.drawText(in: rect.inset(by: insets))
}
@IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
self.layer.cornerRadius = cornerRadius
self.layer.masksToBounds = cornerRadius > 0
}
}
@IBInspectable var marginTop: CGFloat = 0
@IBInspectable var marginBottom: CGFloat = 0
@IBInspectable var marginLeft: CGFloat = 0
@IBInspectable var marginRight: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
var bounds = self.bounds
bounds.size.width += marginLeft + marginRight
bounds.size.height += marginTop + marginBottom
self.bounds = bounds
}
This works but adjusting self.bounds
inside layoutSubviews()
, makes this method to be called again, resulting in a huge loop, CPU spike and memory leak.
Then I tried this:
override var text: String? {
didSet {
let resizingLabel = UILabel(frame: self.bounds)
resizingLabel.text = self.text
var bounds = resizingLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: 500, height: 50), limitedToNumberOfLines: 1)
bounds.size.width += marginLeft + marginRight
bounds.size.height += marginTop + marginBottom
self.bounds = bounds
}
}
this simply does not work. Label is not adjusted to the proper size.
The label must have just one line, fixed height, truncated tail and fixed font size (System 17). I am interested in its width.
Any ideas?
A view should not change its own size. It should only change its intrinsicContentSize
.
When you add a view to the view hierarchy, that’s when you specify whether it should observe the intrinsic content size or not (e.g. content hugging settings, compression resistance, absence of explicit width and height constraints, etc.). If you do this, the auto layout engine will do everything for you.
So, by way of example, a minimalist approach would be something that just overrides intrinsicContentSize
:
@IBDesignable
class BeautifulLabel: UILabel {
@IBInspectable var marginX: CGFloat = 0 { didSet { invalidateIntrinsicContentSize() } }
@IBInspectable var marginY: CGFloat = 0 { didSet { invalidateIntrinsicContentSize() } }
@IBInspectable var cornerRadius: CGFloat = 0 { didSet { layer.cornerRadius = cornerRadius } }
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + marginX * 2, height: size.height + marginY * 2)
}
}
A more complete example might be a UIView
subclass, where the label is a subview, inset by the appropriate margins:
@IBDesignable
class BeautifulLabel: UIView {
@IBInspectable var marginTop: CGFloat = 0 { didSet { didUpdateInsets() } }
@IBInspectable var marginBottom: CGFloat = 0 { didSet { didUpdateInsets() } }
@IBInspectable var marginLeft: CGFloat = 0 { didSet { didUpdateInsets() } }
@IBInspectable var marginRight: CGFloat = 0 { didSet { didUpdateInsets() } }
@IBInspectable var cornerRadius: CGFloat = -1 { didSet { setNeedsLayout() } }
@IBInspectable var text: String? {
get {
label.text
}
set {
label.text = newValue
invalidateIntrinsicContentSize()
}
}
@IBInspectable var font: UIFont? {
get {
label.font
}
set {
label.font = newValue
invalidateIntrinsicContentSize()
}
}
private var topConstraint: NSLayoutConstraint!
private var leftConstraint: NSLayoutConstraint!
private var rightConstraint: NSLayoutConstraint!
private var bottomConstraint: NSLayoutConstraint!
private let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override var intrinsicContentSize: CGSize {
let size = label.intrinsicContentSize
return CGSize(width: size.width + marginLeft + marginRight,
height: size.height + marginTop + marginBottom)
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
let maxCornerRadius = min(bounds.width, bounds.height) / 2
if cornerRadius < 0 || cornerRadius > maxCornerRadius {
layer.cornerRadius = maxCornerRadius
} else {
layer.cornerRadius = cornerRadius
}
}
}
private extension BeautifulLabel {
func configure() {
addSubview(label)
topConstraint = label.topAnchor.constraint(equalTo: topAnchor, constant: marginTop)
leftConstraint = label.leftAnchor.constraint(equalTo: leftAnchor, constant: marginLeft)
rightConstraint = rightAnchor.constraint(equalTo: label.rightAnchor, constant: marginRight)
bottomConstraint = bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: marginBottom)
NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])
}
func didUpdateInsets() {
topConstraint.constant = marginTop
leftConstraint.constant = marginLeft
rightConstraint.constant = marginRight
bottomConstraint.constant = marginBottom
invalidateIntrinsicContentSize()
}
}
Now in this case, I'm only exposing text
and font
, but you'd obviously repeat for whatever other properties you want to expose.
But let’s not get lost in the details of the above implementation. The bottom line is that a view should not attempt to adjust its own size, but rather merely its own intrinsicContentSize
. And it should perform invalidateIntrinsicContentSize
where necessary.