iOS 13:如何在 UIKit / SwiftUI 中调整自定义字体的前导/下降/行高

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

我正在使用自定义字体,不知何故渲染搞砸了行高,可能是因为配置错误 descentleading (?),所以 g 和 j 在渲染文本的最后一行。我认为这可能是这种特定字体的问题,因为 Sketch 也暴露了相关字体的类似问题,但我觉得我对排版测量或字体的了解还不够。我发现 Typographic Concepts 上的这个 Apple 文档页面非常有见地。

我用 FontLab 的测试版本研究了字体本身,顺便说一句,这是我第一次使用它 - 所以我几乎不知道我在看什么。看起来 g 确实低于配置的 descent,这似乎是最后一行。 (?)(参见:Character view in FontLab, showing the descend of the g)

通过 lineSpacing 我可以调整线条本身之间的距离来解决前几行中的问题。我知道 iOS 14 将带来一种在 SwiftUI 中修改 Textleading 的方法。但我需要瞄准 iOS 13,所以这没有用。

我也试过 SwiftUI 的 Text,一个普通的 UILabel.textUILabel.attributedText 自定义的段落样式,但我在那里所做的任何调整似乎都无法缓解这个问题。 视图甚至没有剪裁。仅向框架添加填充根本无济于事。它增加了距离,但 g's 和 j's 仍然被切割。

我能做什么?当最后一行有 g 和 j 时,子类 UILabel 并覆盖 intrinsicContentSize 以添加一些额外的 space?这感觉 a) 脏 b) 鉴于填充没有帮助,它可能无法解决问题?

是字体本身的问题吗?我能以某种方式修补字体而不会使它变得更糟吗?

当我使用较低级别的 API 时,是否有任何方法可以修改字体的前导高度或下降高度?似乎我可以转到 CoreText,因为 CTFontCreateCopyWithAttributes(_:_:_:_:) 是一个候选者,如果我可以通过属性修改前导、行 space 或下降?可以或猴子修补/混合东西而不用射击自己的膝盖吗?我应该只提交 雷达 反馈吗?

您需要使用NSAttributedString而不是String来控制UILabel的行间距。这是示例代码

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)

输出

我知道你在问什么,因为我在使用自定义字体时遇到过同样的问题。我将提供两种解决方案。在我自己的项目中,我按照您的建议重写 intrinsicContentSize 并为高度和宽度添加填充乘数。在我的例子中,字体是面向用户的,所以我有一个包含所有相关信息的结构。仅供参考 Chalkduster 在系统和剪辑中。我也相信这都是字体文件本身的原因。

解决方案一: 示例:

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

然后在我的 UILabel 中,我将其子类化以使用这两个指标

@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)
    }
}

这对我来说是最简单的实现,因为我找到了相应的字体和乘数比例。

解决方案 2:
通过测量字形边界并调整原点 y,您可能能够使绘制发生得稍微高一些。例如,这修复了系统中包含的 Chalkduster 字体的裁剪。

@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)
    }
}

解决方案 2 的结果: 系统

使用字形边界的 PaddingUILabel