An iOS Developer

iOS: Fix label cuts for custom fonts

Font Cut Off

Has it ever occurred to you that a custom font does not look as expected in your app? like some parts of it are cut off? Look at this example, it supposed to be “Blog” but all you see is “Bl”:

Simulator Screen Shot IPhone X 2019 04 17 At 12.30.30 473x1024

Or this one in Farsi (and Arabic) which expected to be “کریم” but the last two characters are cut off completely:

Simulator Screen Shot IPhone X 2019 04 17 At 13.14.42 1 473x1024

The code to create it is pretty simple. I have used a third party library, FontBlaster, to load custom fonts which is available on github.

label = UILabel(frame: CGRect.zero)
let font = UIFont(name: "BleedingCowboys", size: 60)! // We are in debug mode, right?
label.backgroundColor = UIColor.yellow
label.frame.size = CGSize.zero
label.text = "Blog"
let size = label.sizeThatFits(CGSize.init(
width: CGFloat.greatestFiniteMagnitude, 
height: CGFloat.greatestFiniteMagnitude))
label.frame.size = size
label.center = self.view.center
self.view.addSubview(label)

It seems sizeThatFits(:) cannot determine the size correctly for all fonts. To fix this, I found an extension to UIBezierPath which returns a CGPath for an attributed string, you can find it here. This is how you can the path:

let line = CAShapeLayer() 
line.path = UIBezierPath(forMultilineAttributedString: mutabbleAttributedString, maxWidth: CGFloat.greatestFiniteMagnitude).cgPath
line.bounds = (line.path?.boundingBox)!
// We gonna need it later 
let sizeFromPath = CGSize(width: (line.path?.boundingBoxOfPath.width)!, height: (line.path?.boundingBoxOfPath.height)!) 

UIBezierPath(forMultilineAttributedString:, maxWidth:) comes from that extension I mentioned above. Now we can determine the actual size of the label frame, let’s see it in action:

Simulator Screen Shot IPhone X 2019 04 17 At 14.54.07 473x1024

It’s still not exactly what we want, the size seems to be correct but the left inset is not. To solve this last problem, let’s create a custom UILabel class which can set custom inset while drawing the label:

import Foundation
import UIKit

class CustomLabel: UILabel {
    var textInsets = UIEdgeInsets.zero {
        didSet { invalidateIntrinsicContentSize() }
    }
    
    override func textRect(forBounds bounds: CGRect,
 limitedToNumberOfLines numberOfLines: Int) -> CGRect {
        let insetRect = bounds.inset(by: textInsets)
        let textRect = super.textRect(forBounds: insetRect, limitedToNumberOfLines: numberOfLines)
        let invertedInsets = UIEdgeInsets(top: -textInsets.top,
                                          left: -textInsets.left,
                                          bottom: -textInsets.bottom,
                                          right: -textInsets.right)
        return textRect.inset(by: invertedInsets)
    }
    
    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.inset(by: textInsets))
}

How many points we should add to left inset? the difference between the actual width and width from sizeThatFits.First we need to replace the line in which we declared the label. Instead of the UILabel we need to use CustomLabel. Then:

label.textInsets = UIEdgeInsets(top: 0, left: sizeFromPath.width - size.width, bottom: 0, right: 0)

Let’s see the final result:

Simulator Screen Shot IPhone X 2019 04 17 At 15.49.27 473x1024

Nice! yeah? Thing is you might not need the inset for all troublesome fonts, check it yourself

2 Comments

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.