Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS 13: How can I tweak the leading / descend / line height of a custom font in UIKit / SwiftUI

Tags:

I'm using a custom font and somehow the rendering screws up the line height, potentially because of misconfigured descent or leading (?), so that g's and j's are cut off in the last line of the rendered text. I think it might be a problem with this particular font, because Sketch is also exposing similar issues with the font in question, but I feel like I don't understand quite enough about typographic measurements or fonts. I found this Apple documentation page on Typographic Concepts quite insightful.

Rendered Label exposing the cut off

I looked into the font itself with the test version of FontLab, which I have used for the first time btw - so I've little clue really what I'm looking at. It does seem like the g is going below the configured descent, which seems to be what the last line is. (?) (See: Character view in FontLab, showing the descend of the g)

Via lineSpacing I could adjust the distance between just the lines itself to fix this in the first few lines. I know iOS 14 is going to bring a way to modify the leading of a Text in SwiftUI. But I need to target iOS 13, so that doesn't help.

I've also tried SwiftUI's Text, a normal UILabel.text and UILabel.attributedText with a customized paragraph style, but nothing I adjust there seems to mitigate the problem. The view is not even clipping. Just adding padding to the frame does not help at all. It increases the distance, but the g's and j's are still cut.

What can I do? Subclass UILabel and overwrite the intrinsicContentSize to add some extra space, when there is a g and j in the last line? That feels a) dirty and b) given that padding didn't help, it might not fix the problem?

Is the font itself the problem here? Can I patch the font somehow without making it worse?

Is there any way to modify the leading or the descend height of the font, when I use lower level APIs? Seems like I could go down to CoreText, as CTFontCreateCopyWithAttributes(_:_:_:_:) is a candidate, if I could just modify via attributes the leading, line space or the descend? Can or monkey-patch / swizzle things without shooting myself in the knee? Should I just file a radar a feedback?

like image 819
marius Avatar asked Aug 27 '20 19:08

marius


2 Answers

You need to use NSAttributedString instead of String to control the line spacing of UILabel. Here is sample code

let style = NSMutableParagraphStyle()
style.lineSpacing = 20
let string  = NSMutableAttributedString(string: "The quick brown fox jumps over the lazy dog, The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog")
string.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, string.length))
let label = UILabel(frame: CGRect(x: 20, y: 100, width: 300, height: 500))
label.attributedText = string
label.numberOfLines = 0
self.view.addSubview(label)

Out put

enter image description here

like image 64
Vo Kim Nguyen Avatar answered Sep 30 '22 19:09

Vo Kim Nguyen


I know what you are asking as I have faced the same issues with custom fonts. I am going to offer two solutions. In my own project I went the way of your suggestion in overriding intrinsicContentSize and adding a padding multiplier for height and width. In my case the fonts were user facing so I had a struct that held all the relevant information. FYI Chalkduster is in the system and clips. I also believe that this is all due to the font file itself.

Solution 1: Example:

struct UserFont{
    var name : String
    var displayName : String
    var widthMultiplier : CGFloat
    var heightMultiplier : CGFloat
}   

Then in my UILabel I have it subclassed to use both of these metrics

@IBDesignable
class MultiplierUILabel: UILabel {
    @IBInspectable var widthPaddingMultiplier : CGFloat = 1
    @IBInspectable var heightPaddingMultiplier : CGFloat = 1
    override var intrinsicContentSize: CGSize{
       return CGSize(width: super.intrinsicContentSize.width * widthPaddingMultiplier, height: super.intrinsicContentSize.height * heightPaddingMultiplier)
    }
}

This to me was the simplest implementation as I found the font and multiplier scale accordingly.

Solution 2:
You might be able to get the draw to occur slightly higher by measuring the glyph bounds and adjusting the origin y. For example this fixes the clipping on Chalkduster font that is included in the system.

@IBDesignable
class PaddingUILabel: UILabel {
    override func drawText(in rect:CGRect) {
        //hello
        guard let labelText = text else {  return super.drawText(in: rect) }
        //just some breathing room
        let info = boundsForAttrString(str: labelText, font: self.font!, kern: .leastNormalMagnitude)
        let glyph = info.glyph
        var newRect = rect
        let glyphPadding = -(glyph.origin.y)
        if glyphPadding - info.descent > 1 && info.descent != 0{
            newRect.origin.y -= glyphPadding/2
        }else{
            if info.descent != 0{
                newRect.origin.y += (info.descent - glyphPadding)/2
            }
        }
        super.drawText(in: newRect)
    }
    
    func boundsForAttrString(str:String,font:UIFont,kern:CGFloat)->(glyph:CGRect,descent:CGFloat){
        let attr = NSAttributedString(string: str, attributes: [.font:font,.kern:kern])
        let line = CTLineCreateWithAttributedString(attr)
        var ascent : CGFloat = 0
        var descent : CGFloat = 0
        var leading : CGFloat = 0
        
        CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
        let glyph = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds).integral
        return (glyph,leading != 0 ? descent : 0)
    }
}

Result of Solution 2: System System

PaddingUILabel using glyph bounds PaddingUILabel

like image 32
agibson007 Avatar answered Sep 30 '22 19:09

agibson007