☰
iOS多行文本尺寸计算与NSStringDrawingOptions避坑
2026/11/6 2:43:17 网站建设 项目流程

做列表页做得久了,会形成一个条件反射:只要 cell 高度开始忽高忽低、末行文字被切掉半截、或者明明只有一行却撑出两行的高度,先别急着怀疑约束冲突,回头去看那行boundingRectWithSize:options:attributes:context:的 options 是怎么写的,十有八九问题就在那儿。NSStringDrawingOptions这个枚举成员不多,但它里面每一个开关都对应一个很具体的排版概念,写错一个,表现出来就是另一套完全不同的尺寸结果。很多同学从sizeWithFont:那套老 API 迁移过来的时候,习惯性地把 options 写成 0,或者随手抄了个NSStringDrawingUsesLineFragmentOrigin就完事,结果在单行场景下一切正常,一上多行、一换字体、一加行距就开始出问题。

这篇东西就是想把 NSStringDrawingOptions 的每个成员单独拎出来讲清楚:它到底改了什么,为什么会有这个差异,什么场景下必须加、什么场景下加了反而是负担。主要面向正在做多行文本排版、被 cell 高度折腾过的 iOS 开发者,刚入行不久、还没搞明白这套老 API 的同学读完也能自己动手验证;已经有几年经验、平时靠封装好的工具类吃老本的同学,也能在这里找到几个平时容易忽略的边界情况。文中给出的数值和表现,都是我在实际项目里反复验证过的,涉及具体版本差异的地方我会标出来,你在自己的工程里跑一遍对照最稳妥。

1. 先把 NSStringDrawingOptions 的成员摆上桌

1.1 它真正生效的地方只有两处

很多人以为这个 options 是给所有字符串绘制和测量 API 用的,其实不是。真正会读这个参数的方法就两组:一组是boundingRectWithSize:options:attributes:context:和NSAttributedString的同名方法,负责算尺寸;另一组是drawWithRect:options:attributes:context:和NSAttributedString的对应方法,负责画。至于sizeWithAttributes:、drawAtPoint:withAttributes:、drawInRect:withAttributes:这些,压根就没有 options 参数,它们内部用的是默认行为。

这个区分很重要,因为sizeWithAttributes:的语义就是「按单行、不换行、不截断来算」,它等价于boundingRectWithSize传一个无限大的尺寸再加 options 为 0。你在代码里混用这两套方法,就会出现「量的时候按多行算、画的时候按单行画」的错位,这种 bug 特别隐蔽,因为两边单独看都没错。

所以第一条要记住的结论是:尺寸计算和实际绘制必须用同一套 options。你要是用UsesLineFragmentOrigin | UsesFontLeading算出来两行的高度,绘制的时候也得带上这两个选项,否则画出来就是一行,剩下那一行的空间空在那里。

1.2 四个常用成员的职责对照

iOS 上你真正会打交道的成员就是下面这四个,我把值和现象放在一张表里,方便对照:

选项位值一句话作用使用频率漏写后的典型现象
NSStringDrawingUsesLineFragmentOrigin1 << 0把给定的 size 当成一个可换行的矩形区域来排版几乎必写高度永远只有一行,宽度可能远超限定值
NSStringDrawingUsesFontLeading1 << 1计算行高时把字体的 leading 也算进去强烈建议多行时高度偏小,末行下半截被切
NSStringDrawingUsesDeviceMetrics1 << 3用字形墨迹边界代替排版边界参与计算极少一般无感知,开了反而和 UILabel 对不齐
NSStringDrawingTruncatesLastVisibleLine1 << 5放不下时截断最后一行并补省略号按需超出部分被硬裁,没有省略号提示

注意位值这一列:1<<0、1<<1、1<<3、1<<5,中间空了两个位置。这不是随手写的,1<<2 和 1<<4 历史上分配给了NSStringDrawingOneShot和NSStringDrawingDisableScreenFontSubstitution这两个 macOS 时代的成员,在 iOS 业务里基本没有出场机会,后面第 5 章会单独说。

看到这个跳号,其实就能记住一件事:iOS 上你只要盯住这四个就够了,别去网上抄那些带 OneShot 的老代码,抄进来也不会有任何效果。

1.3 为什么这个枚举全是「开关」而没有「参数」

刚接触这套 API 的人常有的疑问是:既然是控制排版的,为什么不能在这里指定行距、指定最大行数?答案在于职责划分——具体的字体、字号、行距、段落样式,全部塞在attributes字典里的NSFontAttributeName和NSParagraphStyleAttributeName中;options 只负责回答一个问题:你给我的那个 size,到底该怎么理解?

理解了这个定位,后面所有的行为就都能推出来了。UsesLineFragmentOrigin说的是「这个 size 是个矩形,不是一个基线」;UsesFontLeading说的是「行高按哪种算法来」;TruncatesLastVisibleLine说的是「放不下的时候怎么办」。全是解释规则,没有一个是数值。

2. UsesLineFragmentOrigin:不写它,多行文本永远只有一行高

2.1 基线原点和行片段原点的差别

这个选项的名字翻译过来有点绕,叫「使用行片段原点」。要理解它,先得知道另一种模式——基线原点(baseline origin)。

基线原点的思路是:文字是站在一条基线上的,你告诉我一个位置,我就从这条基线开始往右写,写多长算多长,不折行。这种模式下,返回的矩形是紧贴着文字墨迹的,它的顶部在基线上方,所以origin.y会是一个负数。很多同学第一次打印boundingRectWithSize的结果,看到{{0, -3.5}, {320, 17.9}}这种值会懵,就是这个原因。

行片段原点的思路完全不一样:你给我的 size 是一个矩形框,我在这框里面排版,宽度到了就折行,一折行就产生一个「行片段」。返回的 rect 从 (0, 0) 开始,高度是所有行片段叠起来的总高度。这才是我们做 cell 高度自适应时想要的东西。

用个生活化的类比:基线原点像是拿一把尺子量「这行字有多长」,行片段原点像是拿一个相框问「这些字塞进这个相框要占多高」。

2.2 一段可以直接跑的复现代码

光说概念没感觉,下面这段代码你复制到工程里跑一下,两个结果的差别一眼就能看出来:

NSDictionary *attrs = @{ NSFontAttributeName: [UIFont systemFontOfSize:15] }; NSString *text = @"这是一段用来测试换行行为的长文本,长度足够在两百点宽度内折成好几行。"; CGSize limit = CGSizeMake(200, CGFLOAT_MAX); CGRect r1 = [text boundingRectWithSize:limit options:0 attributes:attrs context:nil]; CGRect r2 = [text boundingRectWithSize:limit options:NSStringDrawingUsesLineFragmentOrigin attributes:attrs context:nil]; NSLog(@"不加选项: %@", NSStringFromCGRect(r1)); NSLog(@"加了选项: %@", NSStringFromCGRect(r2));

在 15pt 系统字体下,你会看到类似这样的输出:r1的宽度明显超过 200,可能是 400 多,高度大约 17.9,origin.y是个小负数;r2的宽度正好卡在 200 以内,高度是 17.9 的整数倍,origin是 (0, 0)。

这个对照实验建议每个新同学都亲手跑一次,比看十篇博客都管用。因为它把「options 到底在控制什么」这件事具象化了——不是控制字号,不是控制字体,而是控制「你怎么理解我给你的那个尺寸」。

2.3 取尺寸时千万别直接用 size.width

接着上面说,既然返回的 rect 的 origin 不一定是 (0, 0),那取尺寸的正确姿势就只能是CGRectGetWidth(rect)和CGRectGetHeight(rect)。我见过不少代码写成rect.size.width或者更糟,把整个 rect 直接塞进 frame 里,结果控件莫名其妙被偏移了几个点。

这个坑在单行场景下不明显,因为不加选项时 origin 才非零;一旦你开始用行片段原点,origin 就是 (0, 0) 了,代码看起来也没问题。于是这批代码就埋在那儿,等到哪天有人删了 options 或者换了调用方式,问题才爆出来。建议从一开始就统一写成CGSizeMake(CGRectGetWidth(rect), CGRectGetHeight(rect)),习惯成自然。

2.4 CGFLOAT_MAX 这个上限值该怎么给

标准写法是宽度给实际可用宽度,高度给CGFLOAT_MAX。但这里有两个实操细节值得说一下。

第一个是浮点溢出的风险。CGFLOAT_MAX是个极大的数,参与某些字体度量运算时,个别系统版本上会算出inf或者NaN,最终返回的高度变成 0 或者一个离谱的值。我在封装工具类的时候加了一层兜底:如果算出来的高度小于等于 0 或者isnan为真,就退回一个有限的大值(比如 100000)重算一次。这层保护平时用不上,但线上真出过一次问题之后,我就一直留着了。

第二个是取整。boundingRect返回的高度经常是 43.2000001 这种值,你直接拿去设 frame,渲染的时候会因为半像素对齐糊掉一条边,看起来就是文字底部发虚。稳妥的做法是统一向上取整:

static inline CGFloat TextCeilValue(CGFloat value) { CGFloat scale = UIScreen.mainScreen.scale; if (scale <= 0) scale = 2.0; return ceil(value * scale) / scale; }

先乘 scale 再 ceil 再除回去,能保证结果落在物理像素网格上,比单纯ceil()更精细,特别是在 3x 屏上做细线条或者紧凑排版时,差别肉眼可见。

3. UsesFontLeading:最后一行为什么总被吃掉一点

3.1 leading、ascender、descender 三者的关系

要讲清楚这个选项,得先把UIFont的那几个度量属性捋一遍。ascender是基线以上的高度,包含了重音符号预留的空间;descender是基线以下的深度,注意它是个负数;leading是行与行之间的额外留白。三者的关系是:

lineHeight = leading + ascender - descender

而行高其实有两种算法:一种是只用ascender - descender(因为 descender 是负的,实际就是 ascender 加绝对值),另一种是把leading也加上。UsesFontLeading干的事就是告诉系统:算行高的时候,请用第二种。

为什么会有两种?因为字体设计上,leading 是给「上一行的降部和下一行的升部之间」留的安全距离。有些字体这个值是 0,有些字体能到 2 到 3 个点。系统字体在这个值上比较克制,很多时候接近 0,所以你在单行、系统字体的场景下根本看不出差别——这也正是这个 bug 最难查的地方。

3.2 误差会被行数成倍放大

单行差 0.5 个点,你可能觉得无所谓。但这是一个按行数累加的误差,十行就是 5 个点,二十行就是 10 个点。列表里那种长文摘要,正好踩在这个坑上:短文本一切正常,长文本最后一行就被切掉一截,露出来的半个字看着特别难受。

我整理了一个按字体类型的粗略对照,帮助你判断风险等级。具体的绝对数值会随系统版本和字体文件变化,这里给的是量级和累计影响:

字体类型单行 leading 量级10 行累计误差风险判断
系统默认字体(正文场景)接近 0 到 0.5pt0 到 5pt中低,长文本需注意
系统中粗体、标题字号通常略大于正文2 到 8pt中,卡片标题易出问题
第三方西文字体差异较大,常见 1 到 3pt10 到 30pt高,必须显式处理
已设置固定行高的段落被行高设置覆盖取决于设置低,leading 影响被吸收

看这张表能得出一个实用结论:如果你用的是系统字体做正文,单行或少行场景可以不管;只要文本可能超过五六行,或者用了自定义字体,这个选项就必须加上。与其判断用什么字体,不如统一加上,反正加上了也不会有什么副作用。

3.3 它和 lineSpacing 是叠加关系,不是替代关系

很多同学以为设了paragraphStyle.lineSpacing之后 leading 就不起作用了,这是个误解。lineSpacing是段落样式层面的额外间距,加在行高之上;leading是字体自带的度量,属于行高的一部分。两者是叠加的。

所以正确的行高计算方式是:

单行高度 = max(leading + ascender - descender, minimumLineHeight) + lineSpacing

这里maximumLineHeight和minimumLineHeight如果你设了,它会覆盖字体度量算出来的行高,这时候 leading 的意义就被吃掉了——这也是我后面要说的「什么时候可以关掉它」的场景之一。

3.4 什么情况下可以考虑不加 UsesFontLeading

不是说这个选项永远要加。有两种情况我会主动把它去掉,或者说不加也不影响结果。

第一种是你手动设了minimumLineHeight和maximumLineHeight,把行高钉死在一个固定值上。这时候系统直接用你给的值,字体自己的 leading 已经不参与计算了,加不加这个选项结果一样。做严格的行网格对齐(比如要求每行正好 24pt)时,这是常见做法。

第二种是你要和控制台里 UILabel 的实际渲染严格对比。这里要提醒一句:UILabel 内部走的是 TextKit,它的行高就是font.lineHeight,也就是包含 leading 的那个值。所以正常业务里,加上UsesFontLeading算出来的高度反而更贴近 UILabel 的真实表现。你要是去掉了,算出来的高度会比 UILabel 矮一点点,然后你就得在 cell 底部补 padding 来凑,越补越乱。

4. TruncatesLastVisibleLine:省略号什么时候才出现

4.1 它和 lineBreakMode 是两个层级的东西

这是我认为最容易搞混的一对概念。TruncatesLastVisibleLine是一个开关,回答的是「允许不允许截断」;NSParagraphStyle.lineBreakMode回答的是「截断的时候截在哪儿、加不加省略号」。

你可以这么理解:前者是「这扇门能不能锁」,后者是「锁上之后钥匙插哪个孔」。

官方头文件里对这个选项的注释写得很明确:如果不同时设置UsesLineFragmentOrigin,这个选项会被忽略。所以最小可用组合是UsesLineFragmentOrigin | TruncatesLastVisibleLine。

然后是实际表现,我整理了一张对照表:

lineBreakMode设置只加 Truncates加 Truncates 且显式设 lineBreakMode
NSLineBreakByWordWrapping(默认)文字被硬裁,省略号可能不出现仍可能无省略号,不推荐
NSLineBreakByCharWrapping按字符边界裁切无省略号,纯裁切
NSLineBreakByTruncatingTail末尾补省略号正常,推荐写法
NSLineBreakByTruncatingMiddle中间补省略号正常
NSLineBreakByTruncatingHead开头补省略号正常

表里第一行那个「可能」是真实情况——不同系统版本对这种组合的处理不完全一致,有的会补省略号,有的就直接裁掉。这种不确定性对业务代码来说是灾难,所以结论很干脆:要省略号就把lineBreakMode显式写成NSLineBreakByTruncatingTail,别赌默认值。

4.2 想按行数限制,光靠这个选项不够

这是一个特别常见的误区。很多人以为给boundingRectWithSize传个TruncatesLastVisibleLine,再在 attributes 里限制一下,就能得到「最多三行」的高度。不行。

原因在于这个选项的触发条件:它是在「文本放不进你给的 size」时才生效。而你算高度的时候,高度参数给的是CGFLOAT_MAX,永远放得下,自然永远不截断。所以它返回的是完整文本的高度。

想按行数限制,正确做法是先自己算出这个行数对应的高度上限,再把它作为 height 传进去:

CGFloat lineHeight = font.lineHeight + lineSpacing; CGFloat maxHeight = lineHeight * maxLines - lineSpacing; CGSize limit = CGSizeMake(width, maxHeight); NSStringDrawingOptions opts = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading | NSStringDrawingTruncatesLastVisibleLine; CGRect rect = [text boundingRectWithSize:limit options:opts attributes:attrs context:nil];

注意maxHeight那里减了一个lineSpacing。因为lineSpacing只加在行与行之间,最后一行下面不该有间距,n行文本实际只有n - 1个间距。这个减法漏掉,你的高度就会恒定多出一个lineSpacing,在lineSpacing设得比较大的设计稿里(比如 6pt、8pt),肉眼能看出来卡片底部空了一块。

4.3 和 minimumScaleFactor 一起用时的优先级

NSStringDrawingContext里有个minimumScaleFactor,默认是 0,小于 1 的时候才启用缩放。启用之后,系统遇到放不下的情况会先缩字号,缩到这个下限还放不下,才走截断逻辑。

这个优先级顺序意味着:如果你的文本已经能通过缩字号塞进去,那TruncatesLastVisibleLine根本不会触发,你也就拿不到省略号。同时actualScaleFactor会告诉你实际缩放了多少倍,你可以读出来做后续判断。

但在 cell 高度计算的场景里,我建议不要传 context。理由很实际:一旦启用缩放,最终高度就同时依赖于宽度和字号两个变量,宽度变一点点,缩放比例变一点点,高度就跟着跳,列表滚动的时候高度抖动就是这么来的。缩放适合做那种固定尺寸的卡片标题,不适合做自适应高度的列表。

5. UsesDeviceMetrics 与另外两个历史选项

5.1 UsesDeviceMetrics 到底改了什么

这个选项的作用是把计算基准从「排版边界」(typographic bounds)换成「字形墨迹边界」(image glyph bounds)。听起来很玄,说白了就是:排版边界看的是字体的 ascent 和 descent,那些是设计时预留的空间;墨迹边界看的是字符真正画出来的那一圈轮廓。

两者的差别主要体现在几个地方:字符的实际高度往往小于字体预留的高度(因为上下都有留白),所以开了这个选项算出来的高度通常会略小一点;带下伸部的字符(比如小写字母 g、p)和带上伸部的字符(比如 b、d)会让墨迹边界不规则;而中文字符大多是方块字,墨迹几乎填满字身框,所以差异反而不明显。

5.2 为什么业务里基本不用它

我总结出三个不用它的理由,按重要性排:

第一,它会让你算出来的高度和 UILabel 的渲染对不上。UILabel 默认是按排版边界走的,你这边按墨迹边界算,两边基准不同,就差出来了。做 cell 自适应时,差一两个点就是「文字贴边」和「文字留白」的区别。

第二,不同字体的表现不一致。西文字体差异明显,中文字体几乎没差,混排的时候你根本没法预测结果。这种不确定性在业务代码里就是定时炸弹。

第三,它和截断、缩放这些逻辑叠加时的行为没有官方文档描述。你想搞清楚「开着 DeviceMetrics 又开着 Truncates」会怎样,只能自己一个个版本去试,投入产出比太低。

我唯一能想到的合理使用场景是做像素级还原的设计工具,或者需要把文字外框和图片外框严格对齐的特殊排版。普通业务代码,看见这个选项绕道走就对了。

5.3 OneShot 和 DisableScreenFontSubstitution 的时代背景

这两个成员在今天的 iOS SDK 里已经标记为废弃,属于 macOS 早期(十多年前)的产物,理解它们的存在有助于记住位值的来源。

NSStringDrawingDisableScreenFontSubstitution对应位值 1<<4。那个年代显示器分辨率很低,小字号文本渲染时系统会用点阵字体替换矢量字体,让文字更清晰。这个选项就是用来关掉替换、强制用原始字体的。现在屏幕像素密度早就上去了,这套机制基本不存在了。

NSStringDrawingOneShot对应位值 1<<2。它的语义是「只绘制一次,不缓存排版结果」。在字符串内容每次都不一样、缓存反而成负担的场景下有点用。现在这套 API 内部早就换了好几轮实现,这个开关已经没有任何实际效果。

看到这里再回头看第 1 章那张表里空着的 1<<2 和 1<<4,就能串起来了:这两个位置不是没用,是被历史占用了,而占用它们的成员已经退休。你在网上搜到的老代码如果写了NSStringDrawingOneShot,直接删掉,编译器不会有意见,运行结果也不会有变化。

6. 封装一份能直接抄的尺寸计算工具

6.1 核心方法完整实现

把前面几章的内容揉在一起,我给一个自己项目里用了很久的封装。它覆盖了单行、多行自适应、限定行数三种场景,参数都是显式的,方便你在调用处一眼看懂用了什么配置:

+ (CGSize)textSizeForString:(NSString *)text width:(CGFloat)width font:(UIFont *)font lineSpace:(CGFloat)lineSpace maxLines:(NSInteger)maxLines { if (text.length == 0 || width <= 0 || !font) { return CGSizeZero; } NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; style.lineSpacing = lineSpace; style.lineBreakMode = (maxLines > 0) ? NSLineBreakByTruncatingTail : NSLineBreakByWordWrapping; NSDictionary *attrs = @{ NSFontAttributeName: font, NSParagraphStyleAttributeName: style, }; NSStringDrawingOptions opts = NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading; CGFloat limitHeight = CGFLOAT_MAX; if (maxLines > 0) { opts |= NSStringDrawingTruncatesLastVisibleLine; CGFloat lineHeight = font.lineHeight + lineSpace; limitHeight = lineHeight * maxLines - lineSpace; } CGSize limit = CGSizeMake(width, limitHeight); CGRect rect = [text boundingRectWithSize:limit options:opts attributes:attrs context:nil]; CGFloat resultWidth = CGRectGetWidth(rect); CGFloat resultHeight = CGRectGetHeight(rect); if (!isfinite(resultHeight) || resultHeight <= 0) { CGRect fallback = [text boundingRectWithSize:CGSizeMake(width, 100000) options:opts attributes:attrs context:nil]; resultWidth = CGRectGetWidth(fallback); resultHeight = CGRectGetHeight(fallback); } return CGSizeMake(TextCeilValue(resultWidth), TextCeilValue(resultHeight)); }

几个设计决策解释一下。maxLines传 0 表示不限行数,此时高度上限给CGFLOAT_MAX,得到的是完整文本高度;传大于 0 的值时,先算出行数对应的高度上限,再交给系统去截断,同时把lineBreakMode切成尾部省略。那个isfinite判断就是第 2 章提到的浮点兜底,代码里留着不碍事,真出事的时候能救你一命。

另外要注意font.lineHeight这个值已经包含了 leading,所以lineHeight = font.lineHeight + lineSpace这个算法是正确的。如果你手动设了minimumLineHeight,那就得用你设的值代替font.lineHeight,别直接套公式。

6.2 参数选择速查表

封装有了,但调用的时候 options 怎么选还是得有依据。下面这张表我贴在过团队文档里,直接按场景查就行:

业务场景options 组合height 上限lineBreakMode
单行标签(姓名、金额)0无所谓不设置
多行高度自适应(列表摘要)LineFragmentOrigin + FontLeadingCGFLOAT_MAXWordWrapping
固定三行 + 尾部省略号LineFragmentOrigin + FontLeading + Truncates三行对应高度TruncatingTail
只裁剪不补省略号LineFragmentOrigin + FontLeading指定高度WordWrapping
固定行高网格排版只加 LineFragmentOriginCGFLOAT_MAXWordWrapping

最后一行那个组合值得多说一句:既然你已经用minimumLineHeight把行高钉死了,UsesFontLeading确实可以省掉,少一个变量少一份心智负担。

6.3 和 UILabel、UIButton、cell 怎么对接

算出来的尺寸最终要落到控件上,这几个对接点各有各的坑。

UILabel 的话,记住两件事:numberOfLines = 0才能多行,preferredMaxLayoutWidth不设的话intrinsicContentSize永远按单行算。第二个坑特别常见,尤其是在 Auto Layout 里,约束明明写对了但高度就是不对,八成是没设preferredMaxLayoutWidth。

UIButton 稍微麻烦一点。iOS 15 之后引入了配置化的UIButtonConfiguration,按钮标题的多行行为和以前不一样了。用老 API 的话,要设button.titleLabel.numberOfLines = 0,然后用sizeThatFits:或者自己算。混用新旧两套的时候,标题换行可能完全不生效,这时候去看官方文档里关于 configuration 的说明,比自己瞎试快。

cell 高度的原则只有一条:要么全手算,要么全交给 Auto Layout,不要一半一半。手算就是用heightForRowAtIndexPath加上面那个封装;交给 Auto Layout 就用systemLayoutSizeFittingSize:配合UITableViewAutomaticDimension。混着来的典型表现是:滚动的时候高度会跳一下,因为首次布局用的是估算值,滚过去之后才换成真实值。这个现象排查起来非常费时间,从一开始就统一方案最省事。

7. 高度不对时的排查顺序

7.1 现象到原因的速查表

线上出了问题,按这张表从上往下对,基本能定位到八成的情况:

现象最可能的原因处理方式
高度永远只有一行漏了UsesLineFragmentOrigin补上,同时检查绘制处是否一致
多行时末行被切漏了UsesFontLeading补上,或确认字体 leading 是否为 0
宽度超出容器边界options 传了 0补UsesLineFragmentOrigin
超长文本没有省略号只加了 Truncates,lineBreakMode仍是 WordWrapping显式设成TruncatingTail
高度比 UILabel 实际渲染多出几 ptlineSpacing边界差异手算时减一个lineSpacing
高度时大时小context 里传了minimumScaleFactor计算尺寸时不传 context
高度恒等于某个固定值height 上限传了固定数值改成CGFLOAT_MAX

最后一条我再强调一次,因为它太容易被忽略:boundingRectWithSize的 height 参数是上限,不是期望值。我曾经接手一个模块,封装里把高度写死成 100(大概是当初为了防溢出随手填的),结果所有超过三行的长文高度全算成 100,列表里所有长文本都被切在同一位置,看起来像是「系统统一截断」,实际就是这一个数字的锅。

7.2 几个我实际踩过的坑

第一个坑是图文混排。项目里有一段带NSTextAttachment的富文本,文字里嵌了个小图标。用普通的boundingRect算出来的高度没把附件的尺寸完全算进去,导致最后一行文字被图标压住半个字。解决办法是把附件所在那段的行高用minimumLineHeight撑开,或者干脆把这类内容拆成两行来排。附件尺寸参与行高计算这件事,在不同系统版本上表现不完全一致,做之前一定要在目标版本上实测。

第二个坑发生在按行数限高的时候。当时的设计是「摘要最多两行,超出显示省略号」,我按font.lineHeight * 2算高度上限,上线之后发现卡片底部总是空出来 4 个点,正好是lineSpacing的值。原因就是前面说的,n行只有n - 1个间距,我多算了一个。改成lineHeight * 2 - lineSpacing之后严丝合缝。这种问题在设计稿上完全看不出来,只有拿真机截图去量像素

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询