Custom Label that Resizes According to its Contents
I'm making a label from scratch. (My end purpose is to make a vertical text label for Mongolian script, but for now I am just making a normal horizontal text label as practice.)
As long as there are no length and width constraints, I want my custom label's frame to resize according to its intrinsic content size.
It Works in IB
In IB everything seems to be working fine when I test a UILabel
, my custom label (a UIView
subclass), and a button.
I add top and leading constrains to each of these views but don't set any height or width constraints.
If I resize them on the storyboard (but still without adding any more constraints)...
And then choose Update Frames for All Views in View Controller, the normal label and my custom label both resize properly to their intrinsic content sizes.
It Doesn't Work on a Running App
When I change the label text at runtime, though, my custom label's frame is not resizing. (I temporarily added a dark blue border to the text layer to help differentiate it here from the custom label's frame, which is the light blue background color.)
Clicking "Change text" gives
As you can see, the text layer frame changed but the custom view's frame didn't.
Code
This is my custom label class:
import UIKit
@IBDesignable
class UILabelFromScratch: UIView {
private let textLayer = CATextLayer()
@IBInspectable var text: String = "" {
didSet {
updateTextLayerFrame()
}
}
@IBInspectable var fontSize: CGFloat = 17 {
didSet {
updateTextLayerFrame()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
// Text layer
textLayer.borderColor = UIColor.blueColor().CGColor
textLayer.borderWidth = 1
textLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(textLayer)
}
// MARK: Other methods
override func intrinsicContentSize() -> CGSize {
return textLayer.frame.size
}
func updateTextLayerFrame() {
let myAttribute = [ NSFontAttributeName: UIFont.systemFontOfSize(fontSize) ]
let attrString = NSMutableAttributedString(string: self.text, attributes: myAttribute )
let size = dimensionsForAttributedString(attrString)
textLayer.frame = CGRect(x: self.layer.bounds.origin.x, y: self.layer.bounds.origin.y, width: size.width, height: size.height)
textLayer.string = attrString
}
func dimensionsForAttributedString(attrString: NSAttributedString) -> CGSize {
var ascent: CGFloat = 0
var descent: CGFloat = 0
var width: CGFloat = 0
let line: CTLineRef = CTLineCreateWithAttributedString(attrString)
width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, nil))
// make width an even integer for better graphics rendering
width = ceil(width)
if Int(width)%2 == 1 {
width += 1.0
}
return CGSize(width: width, height: ceil(ascent+descent))
}
}
And here is the view controller class:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var normalLabel: UILabel!
@IBOutlet weak var labelFromScratch: UILabelFromScratch!
@IBAction func changeTextButtonTapped(sender: UIButton) {
normalLabel.text = "Hello"
labelFromScratch.text = "Hello"
}
}
Question
What is it that UILabel
does that I am missing in my custom label? I overrode intrinsicContentSize
:
override func intrinsicContentSize() -> CGSize {
return textLayer.frame.size
}
What else do I need to do?
I was missing a single line of code:
I added it after updating the text layer frame.
The documentation says
Where I found this solution: