Swift tips: NSAttributedString 的 baselineOffset

本文介绍如何使用 baselineOffset 使 NSAttributedString 中的图片和文字在垂直方向上对齐。

NSAttributedStringKey.baselineOffset

为了在一个多行文字的 UILabel 首部插入一个 icon,使用了 NSAttributedString 实现:

let attributes = [  
    NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17.0),
    NSAttributedStringKey.foregroundColor: UIColor.black,
]

let attachment = NSTextAttachment()  
attachment.image = #imageLiteral(resourceName: "picture")  
attachment.bounds = CGRect(  
    x: 0, y: 0,
    width: attachment.image!.size.width, 
    height: attachment.image!.size.height
)
let attributedStringWithImage = NSAttributedString(attachment: attachment)

let string = NSMutableAttributedString()  
string.append(attributedStringWithImage)  
string.append(NSAttributedString(string: " hi there", attributes: attributes))

label.attributedText = string  

效果如下(Label 的 backgroundColor 为灰色,右侧数字是 label 的高度): alt  发现图片位置稍微有些偏上,因为在 NSAttributedString 中,attachment 和 text 的垂直对齐方式基于 baseline,可以利用相关属性设置:

NSBaselineOffsetAttributeName

The baseline offset attribute is a literal distance, in pixels, by which the characters should be shifted above the baseline (for positive offsets) or below (for negative offsets).

添加代码:

let attributesWithBaseline: [NSAttributedStringKey : Any] = [  
    NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17.0),
    NSAttributedStringKey.foregroundColor: UIColor.black,
    NSAttributedStringKey.baselineOffset: 2,
]

let string = NSMutableAttributedString()  
string.append(attributedStringWithImage)  
string.append(NSAttributedString(string: " hi there", attributes: attributesWithBaseline))

label.attributedText = string  

效果如下:

可以看到第二个 Label 中文字向上偏移了 2pt,而 Label 的高度也增加了 2pt。图片的位置没有变化。

UILabel 的 lines 设置为 2,然而第二行内容不见了。在 Debug view hierarchy 中发现第二种情况下 Label 高度比第一种要高,又做了一些实验,不太明白为什么倒数第二种情况第二行文字不见了。最后解决方式是利用 NSTextAttachment 的 bounds 属性来设置图片的偏移。在 iOS 11 中 UILabel 的 attributedText 设置了 baselineoffset 后无法显示多行的 Bug 已经被修复,如果希望文字保持不变,移动图片的位置,也可以使用 NSTextAttachment 的 bounds 来解决。

具体代码见 Demo 工程

CSS: Line box 中的 Baseline

用到 baselineOffset 属性是因为在网页开发中了解过 baseline 有关的内容。

对于浏览器而言,网页中的每个元素生成大于等于零个有宽度、高度和定位的盒子,同时每个盒子可以作为容器(Container),放置盒子。假设盒子 A 被放置在盒子 B 内,那么称 B 为 A 的容器盒(Containing box),A 的定位相对于 B。 容器盒内的盒子们有两种分布方式:盒子横向从左至右排布,当超过父元素宽度时会自动形成新的一行;元素垂直由上向下排布,每个元素占一行。一般情况下(in-flow elements),任意一个容器盒内只存在一种排布方式。

在第一种分布方式中,每一行盒子将形成一个 Line box。

这儿有一些文字,每一行形成了一个 Line box

Line box 之间没有间距也不会重叠,任意一个 Line box 的高度总能容纳它包含的所有盒子。CSS 属性 vertical-align 决定了 Line box 内的盒子与 Line box 在垂直方向的对齐方式,默认值为 baseline。每种西文字体在设计时都定义了 Font metrics,其中属性 Ascender 和 Descender 分别代表字体在 baseline 之上的高度和在 baseline 之下的高度。

在 CSS 2.1 中,对于 Line box 的 baseline 的位置没有明确定义,但 baseline 的位置要让 Line box 的高度最小化。

题外话

在 iOS 开发中突然可以利用 CSS 知识解决问题,想到了这段话:

Again, you can't connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future. You have to trust in something - your gut, destiny, life, karma, whatever. This approach has never let me down, and it has made all the difference in my life.

参考