I am trying to create a custom label that has animatable properties so I decided to use CATextLayer instead of going straight to CoreText..
I came up with the following code (I used playground to test things):
//: A UIKit based Playground for presenting user interface import UIKit import PlaygroundSupport public extension CGRect { public static func centerRect(_ rectToCenter: CGRect, in rect: CGRect) -> CGRect { return CGRect(x: rect.origin.x + ((rect.width - rectToCenter.width) / 2.0), y: rect.origin.y + ((rect.height - rectToCenter.height) / 2.0), width: rectToCenter.width, height: rectToCenter.height) } } class MyLabel : UIView { private let textLayer = CATextLayer() public var textColor: UIColor = UIColor.black public var font: UIFont = UIFont.systemFont(ofSize: 17.0) private var _lineBreak: NSLineBreakMode = .byTruncatingTail init() { super.init(frame: .zero) self.text = nil self.textAlignment = .natural self.lineBreakMode = .byTruncatingTail self.textLayer.isWrapped = true self.layer.addSublayer(self.textLayer) } @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } //Update the CATextLayer's attributed string when this property is set.. public var text: String? { //Getter just returns the CATextLayer's string get { if let attrString = self.textLayer.string as? NSAttributedString { return attrString.string } return self.textLayer.string as? String } //Setter creates an attributed string with paragraph style.. //Font and colour.. set { if let value = newValue { let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle paragraphStyle.alignment = self.textAlignment paragraphStyle.lineBreakMode = self.lineBreakMode self.textLayer.string = NSMutableAttributedString(string: value, attributes: [ .foregroundColor: self.textColor, .font: self.font]) } else { self.textLayer.string = nil } } } //Convert NSTextAlignment to kCAAlignment String for CATextLayer public var textAlignment: NSTextAlignment { get { switch self.textLayer.alignmentMode { case kCAAlignmentLeft: return .left case kCAAlignmentCenter: return .center case kCAAlignmentRight: return .right case kCAAlignmentJustified: return .justified default: return .natural } } set { switch newValue { case .left: self.textLayer.alignmentMode = kCAAlignmentLeft case .center: self.textLayer.alignmentMode = kCAAlignmentCenter case .right: self.textLayer.alignmentMode = kCAAlignmentRight case .justified: self.textLayer.alignmentMode = kCAAlignmentJustified default: self.textLayer.alignmentMode = kCAAlignmentNatural } } } //Convert NSLineBreakMode to kCAAlignmentMode String public var lineBreakMode: NSLineBreakMode { get { return _lineBreak } set { _lineBreak = newValue switch newValue { case .byWordWrapping: self.textLayer.isWrapped = true self.textLayer.truncationMode = kCATruncationNone case .byCharWrapping: self.textLayer.isWrapped = true self.textLayer.truncationMode = kCATruncationNone case .byClipping: self.textLayer.isWrapped = false self.textLayer.truncationMode = kCATruncationNone case .byTruncatingHead: self.textLayer.truncationMode = kCATruncationStart case .byTruncatingMiddle: self.textLayer.truncationMode = kCATruncationMiddle case .byTruncatingTail: self.textLayer.truncationMode = kCATruncationEnd } } } //Override layoutSubviews to render the CATextLayer.. internal override func layoutSubviews() { super.layoutSubviews() //Calculate attributed string size.. let string = self.textLayer.string as! NSAttributedString var rect = string.boundingRect(with: self.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) //If you change dy to 1.0, Text will render weirdly! rect = rect.insetBy(dx: 0.0, dy: 0.0) //Render the textLayer by centering it in its parent.. self.textLayer.contentsScale = UIScreen.main.scale self.textLayer.rasterizationScale = UIScreen.main.scale self.textLayer.frame = CGRect.centerRect(rect, in: self.bounds) self.textLayer.backgroundColor = UIColor.lightGray.cgColor } //Somehow always returns 21.. ={ //Not used right now because it doesn't work.. at all.. private func sizeOfTextThatFits(size: CGSize) -> CGSize { if let string = self.textLayer.string as? NSAttributedString { let path = CGMutablePath() path.addRect(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) let frameSetter = CTFramesetterCreateWithAttributedString(string as CFAttributedString) let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil) let lines = CTFrameGetLines(frame) as NSArray var lineWidth: CGFloat = 0.0 var yOffset: CGFloat = 0.0 for line in lines { let ctLine = line as! CTLine var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 var leading: CGFloat = 0.0 lineWidth = CGFloat(max(CTLineGetTypographicBounds(ctLine, &ascent, &descent, &leading), Double(lineWidth))) yOffset += ascent + descent + leading; } return CGSize(width: lineWidth, height: yOffset) } return .zero } } class MyViewController : UIViewController { override func loadView() { let view = UIView() view.backgroundColor = .white let label = MyLabel() label.backgroundColor = UIColor.red label.frame = CGRect(x: 150, y: 200, width: 200, height: 200) label.text = "Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!" label.textColor = .black view.addSubview(label) self.view = view } } // Present the view controller in the Live View window PlaygroundPage.current.liveView = MyViewController() The problem is that if I use NSParagraphStyle, it will NOT render or calculate the size properly at all!
If I remove the paragraph style, it renders fine but never obeys the line-break mode and the sizing is wrong..
Any ideas what I'm doing wrong? How can I get it to obey the line-break mode with paragraph style? Why does it ALWAYS calculate the size as 21 when paragraph style is applied?
When removing Paragraph Style: 
When removing Paragraph Style but settings TruncateEnd on the CATextLayer (it never truncates and the last line has way more spacing between it and the second-last line): 

setNeedsDisplayand laying out whenever a property changes. No difference.