3

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 using Paragraph Style: enter image description here

When removing Paragraph Style: enter image description here

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): enter image description here

Community
  • 1
  • 1
Brandon
  • 22,723
  • 11
  • 93
  • 186
  • Have you tried moving adding the textLayer somewhere further down the layout chain? (or checking what the frame is at the time of drawing) – solenoid Jan 28 '18 at 03:51
  • @solenoid; Yeah, I tried moving it and I've checked the frame when drawing it. I actually coloured the frame grey and coloured its parent red. I have tried `setNeedsDisplay` and laying out whenever a property changes. No difference. – Brandon Jan 28 '18 at 04:10
  • I played around with it for a while and could not get it to display any better. I remember trying to do something similar and running in to weirdness but that led me to: https://stackoverflow.com/questions/5494498/how-to-control-the-line-spacing-in-uilabel If you look at the strikethrough answer, that may actually be the issue (do stuff in drawTextInRect) I don't know the animations you need to do, but any way to let it draw normally then animate properties above all the nonsense? – solenoid Jan 28 '18 at 15:24
  • so the issue has something to do with the ... glyphs added and it appending them to the last line believe it or not. It's like they don't add a space for the truncated end one. I can share my playground to prove this if you want. for the paragraph style I don't know what the answer is. it is almost like CATextLayer does not support it. I played with this some time ago. What are you going to use paragraph style for? Centering. If so I know of some ways. – agibson007 Jan 30 '18 at 15:48

1 Answers1

0

I've discovered that the end-ellipsis glyph is not taking it's size from the attributed string but rather the CATextLayer.fontSize property.

This seems to default to a large value which explains your text being shifted down and clipped in your first example. Expand the text layer's frame size downward, set CATextLayer's isWrapped=false, and you will see your text plus a huge ... ellipis glyph!

Setting the CATextLayer.fontSize to match the font size of your attributed text works around this issue, but it looks like an Apple bug.

I can't get it work by setting attributed string properties alone either..

Kevin Machado
  • 4,141
  • 3
  • 29
  • 54