STULabel 是一个开源的 iOS 框架,为 Swift 和 Objective-C 提供了标签视图(STULabel
)、标签层(STULabelLayer
)以及用于线程安全文本布局和渲染的灵活 API(《STUShapedString
》、《STUTextFrame
》)。该框架在 Core Text API 的底层部分之上使用 Objective-C++ 实现。STULabel 有一个 Swift 封装框架(STULabelSwift),它提供了一个方便的 Swift API。
目录
STULabel 特性
- 比
UILabel
和UITextView
更快 - 可选的异步布局和渲染
- 支持预渲染(例如,在集合视图的预加载数据处理程序中很有用)
- 对于非常大的标签,自动切换到高效的平铺渲染
- 使用颜色或装饰的文字高亮显示,无需重新布局文本
- 快速文本自动缩放(“缩小到大小”)
- 具有完整
UIDragInteraction
支持的交互式超链接 - 非常灵活的文本截断,包括支持嵌入链接的截断标记以及支持多个垂直堆叠的截断范围
- 支持自动布局
- 支持动态类型
- 支持 UIAccessibility
- 全面支持从右至左的文字
- 可配置的垂直对齐和内容边距
- 对文本布局的精细控制,包括支持固定的基线距离和第一个基线偏移
- 可定制的自动分词
- 文字附件(内联图像)
- 带有准确的下降间隙的下划线
- 灵活的背景装饰,例如带圆角的背景
- 易于查询的丰富文本布局信息
源代码中包含一个您可以使用包含的 Xcode 项目构建的演示应用程序。该演示应用程序包含
- 一个用于查看《世界人权宣言》的查看器,支持39种不同文字系统的语言查看文档。您可以尝试字体、间距、文本装饰、链接、截断等,并比较由
STULabel
渲染的文本和UITextView
渲染的文本。 - 一个用于测试
UITableView
滚动性能的压测工具,让您比较STULabel
、UILabel
和UITextView
的性能,并观察启用或禁用自适应当布局、异步渲染或预加载布局/渲染的效果。 - 一个微型基准测试,让您测量和比较在不同测试用例下
STULabel
、UILabel
和UITextView
的布局和渲染性能。 - 一个微型基准测试,让您测量和比较在不同测试用例下
STUTextFrame
、NSStringDrawing
和Text Kit的布局和渲染性能。 - 一个实现了“点击阅读更多”功能的视图,使用
STULabel
实现。
状态
STULabel是预发布(“beta”)软件。它有错误,需要更多的测试和文档,但它可能已经足够好用于您的目的。如果您想用于任何 serious,请订阅 bug 追踪器并经常更新。
在1.0版本发布之前,API和行为的稳定性应该是大部分稳定的。(二进制接口(ABI)的稳定性不是这个开源库的明确目标。)
许可
除非另行注明,此存储库中的所有内容均在LICENSE.txt中按照2项条款BSD许可进行分发。
STULabel库包含来自Unicode字符数据库的数据,该数据库在Unicode,Inc. 许可协议下分发。
整合
CocoaPods整合
如果您想从Objective-C代码中使用STULabel,请将以下内容添加到您的Podfile中:
pod 'STULabel', '~> 0.8.8'
如果您想从Swift代码中使用STULabel,请将以下内容添加到您的Podfile中:
pod 'STULabelSwift', '~> 0.8.8'
STULabel是STULabelSwift的依赖项。
手动集成
- STULabel项目包含用于构建STULabel和STULabelSwift的单独方案,无论是动态框架还是静态框架。
- 如果您想从Swift代码中使用STULabel,请只链接STULabelSwift。
- 如果您想使用静态框架(s),需要手动将
STULabelResources.bundle
产品添加到您的应用程序或框架目标中。(资源包包含默认链接操作表单的本地化字符串。) - 将STULabel手动集成到您的Xcode项目中的方法如下
-
如果已打开,请关闭Xcode中的STULabel项目。
-
如果未打开,请打开您的项目。
-
将STULabel项目从Finder拖动到您的项目Xcode窗口的项目导航器面板中。
-
在项目导航器中展开
STULabel.xcodeproj
下的子树,然后展开Products
组中的项。现在您应该看到两个STULabel.framework
项、两个STULabelSwift.framework
项、一个STULabelResources.bundle
和一些其他项。名称相同的框架项是相应框架的动态和静态构建。您可以通过Xcode文件检查器(在右侧Xcode窗格)中的完整路径来确定静态框架。例如,静态STULabel.framework
的完整路径以'-static/STULabel.framework
'结尾。 -
在Xcode项目导航器窗格顶部选择您的项目。
-
在中心视图中选择您的应用程序或框架目标。
-
在中心视图中选择“通用”选项卡。
-
如果您想使用动态框架(s)
- 从项目导航器窗格中的
STULabel.xcodeproj
的“Products”组拖动非静态的STULabel.framework
到中心视图的“嵌入式二进制”部分。当您将其拖动到那里时,该框架也会出现在“链接的框架和库”列表中。 - 如果您想使用Swift,以相同的方式使用非静态的
STULabelSwift.framework
。
如果您想使用静态框架(s)而不是
- 将以下项目添加到“链接的框架和库”部分,例如,通过单击“+”按钮并选择相应的项目
- 来自静态目标的
STULabel.framework
, - 如果您想使用Swift,来自静态目标的
STULabelSwift.framework
, - 除非该库之前已添加,否则添加
libc++.tbd
。
- 来自静态目标的
- 在中心视图中选择“构建阶段”选项卡。
- 将您刚刚添加到链接框架列表中的静态
STULabel(Swift)
框架也添加到“目标依赖项”列表。 - 将
STULabelResources
添加到“目标依赖项”。 - 展开“复制资源包资源”部分。
- 将
STULabelResources.bundle
从STULabel.xcodeproj
的“Products”组拖动到“复制资源包资源”部分。 - 在中心视图中选择“构建设置”选项卡。
- 将
$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-static
添加到“框架搜索路径”。
- 从项目导航器窗格中的
-
LLDB格式化器
STULabel源代码中包含了一个LLDB Python 脚本,用于定义由库定义的各种类型的数据格式化程序。如果在Xcode中逐步执行STULabel代码时,导入此脚本将提高您的调试体验。
支持
如果您发现了一个bug,请创建一个GitHub问题。如果可能,请提供重现该问题的示例代码。
如果您对STULabel的使用有疑问,例如如何完成特定的文本布局,请在Stack Overflow上提问,并使用“STULabel”标签。
一些特性细节
性能
使用STULabel
进行同步布局和渲染比UILabel
和UITextView
更快,有时要快几倍。STULabel
有多快取决于具体的使用情况和设备及iOS版本。演示应用程序包含了一个针对标签视图的微基准测试,让您能够比较您自己的设备上不同测试用例下STULabel
、UILabel
和UITextView
的性能。
STULabel
比UILabel
更快主要是因为它更积极地缓存文本布局数据。这在一定程度上是因为UILabel
使用NSStringDrawing
进行布局和渲染,它不支持持续计算出的文本布局,而STULabel
使用的是STUTextFrame
API(基于在Core Text的CTTypesetter
之上的实现),这使得将文本塑形和布局与文本渲染分离变得非常容易。
UITextView
似乎主要用于懒加载大型可变文本并进行细粒度的布局过程自定义,而不是用于显示小型静态字符串。
在《STULabel》中自动文本缩放实现特别快速,因为它不是缩小字体大小,而是在绘图时将布局宽度放大然后缩小内容。这有一个优点,即不需要重新创建属性字符串,字体对象缓存负载较少,大多数文本形状只需要执行一次。iOS上Core Graphics渲染质量在这两种方法上应该相等。
异步渲染
文本布局和渲染可以组成主线程在布局和渲染上花费总时间的很大一部分,尤其是如果文本是用如印地语或阿拉伯语这样的复杂脚本编写的。在《STULabel》中的异步布局和渲染支持使得将其中一部分工作移至后台线程变得容易。(例如,图像解码和绘制可能是iOS上主线程性能瓶颈的另一个原因,但将其移至后台线程通常比文本布局和渲染简单得多。)
演示应用中的《UITableView》示例可以让你启用或禁用《STULabel》视图的异步和预渲染,从而让你可以观察到这些功能对滚动性能的影响。(如果你使用的是快速设备,你可能需要增加自动滚动速度才能使差异明显。)
您可以通过将displaysAsynchronously
属性设置为true来为《STULabel》视图启用异步渲染。
完整地异步执行文本布局要复杂一些,因为您需要在例如收藏视图预取处理程序中进行,并且您需要提前知道所有相关的布局参数,以便配置STULabelPrerenderer
对象。
然而,通常您不需要在后台线程上执行完全的布局,以实现绝对平滑的60或120 FPS滚动。预先仅通过构建您想显示的属性字符串的STUShapedString
实例来完成文本形状,然后用形状字符串而不是属性字符串配置标签视图,就可以显著提高布局性能。
在某些情况下,异步渲染可能会损害用户体验,例如当它导致可见闪烁或中断动画。当《STULabel》可以轻松检测到这种情况时,例如在UIView
动画块中启动布局时,它会自动切换到同步渲染。如果这种自动行为不足够,您可以通过代理接口暂时禁用异步渲染。
弹性文本截断
-
STULabel 允许您指定用作截断标记的属性字符串。这样,例如,您可以使用“……”来省略中文文本。或者,您可以通过将字符串设置为
"… more"
作为截断标记,并设置链接属性,以及当链接被点击时将maximumNumberOfLines
设置为 0 来实现“点击查看更多”的功能。 -
STULabel 允许您自定义文本中的可能截断点。如果您,例如,想要防止在某些单词的中间或空格后进行截断,您可以设置一个
truncationRangeAdjuster
函数来不接受这样的位置。 -
STULabel 允许您在同一个属性字符串中指定多个截断范围。这样,您可以在同一个标签中显示多段文本,否则您可能需要将文本分成多个标签,这简化了您的布局代码并提高了性能。
例如,您可以在单个
STULabel
中显示推文的完整内容,通过将文本的第一行(带用户名)指定为单独的截断范围,这样在必要的情况下用户名会被截断,但时间戳和后续段落会保持完好。示例应用中的UITableView
就这样为“社交媒体”测试用例做了处理。
与 UILabel 和 UITextView 相比的限制和区别
没有 Interface Builder 支持
Xcode 的 Interface Builder 不支持 UIFont
、NSAttributedString
、UIEdgeInsets
或任何枚举类型作为 IBInspectable
属性的类型,因此目前没有方法使 STULabel
能像 UILabel
或 UITextView
一样在 IB 中工作。
如果您可以接受 IBInspectable
的限制,例如,因为您的应用程序仅使用一组固定的“样式”,当然可以扩展 STULabel
并将其子类设置为 IBDesignable
。
自动布局支持
UIKit 中 UILabel
和 UITextView
自动布局的支持广泛使用了私有 API。因此,STULabel 的自动布局支持存在某些限制。
-
自动布局对内嵌内容高度非线性依赖于布局宽度的视图原生不支持。为了支持多行文本视图在自动布局中,UIKit提供了一个私有API,允许
UILabel
和UITextView
参与一个特殊的两遍布局过程。在复杂情况下,这种未记录的两遍布局的结果有时可能令人不满意。STULabel
无法参与两遍布局,因此采用不同的方法:通过计算在无限宽度和当前视图宽度下文本的大小,然后从这两个尺寸中选取最大宽度和高,来计算内嵌入内容大小。当视图宽度变化时,内嵌入内容大小将被标记为无效,并强制UIKit布局算法更新布局。这种方法即使在复杂情况下也似乎工作可靠。 -
UIView.systemLayoutsSizeFitting(...)
方法仅根据子视图约束来计算视图的大小。它们不会调用任何layoutSubviews
方法,因此如果子视图层次结构中的任何视图依赖于手动布局代码,则通常无法确定正确的视图大小。由于STULabel
的自动布局支持依赖于完整的UIKit布局过程,包括对layoutSubviews
的调用,所以对于包含多行STULabel
子视图的视图,除非标签已经有正确的宽度,否则systemLayoutsSizeFitting
将不会计算正确的尺寸。(UILabel
和UITextView
没有这个问题,因为它们受到前面提到的特殊两遍布局处理。)如果你在自己的代码中调用
systemLayoutsSizeFitting
,你可以用对父视图的layoutIfNeeded
调用(如果需要,可以通过临时将视图添加到父视图)来替换它。UITableView
和UICollectionView
使用systemLayoutsSizeFitting
来自适应单元格大小。为了让这适用于包含STULabel
视图的单元格,可以创建UITableViewCell
/UICollectionViewCell
的子类并重写systemLayoutSizeFitting
,如下所示:public override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority hp: UILayoutPriority, verticalFittingPriority vp: UILayoutPriority) -> CGSize { self.layoutIfNeeded() return super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: hp, verticalFittingPriority: vp) }
layoutIfNeeded()
调用确保当调用systemLayoutsSizeFitting
时,所有标签已经具有正确的宽度。(当UITableView
调用此方法时,单元格的宽度已经匹配targetSize.width
。如果没有,你可以在调用self.layoutIfNeeded
之前调整单元格的边界。) -
由于UIKit私有API的限制,从
viewForFirstBaselineLayout
或firstBaselineAnchor
属性返回STULabel
可能不会产生期望的效果。然而,如果您通过重写firstBaselineAnchor
和lastBaselineAnchor
并且将对应的锚点从标签子视图中传递,基线约束应该能按预期工作。 -
iOS 11中引入的体系空间约束(例如,使用
constraint(equalToSystemSpacingBelow:multiplier:)
创建)与STULabel
视图不兼容,因为它们依赖于私有UIKit API。这些约束的确切行为未记录。iOS 12的实现只计算依赖于任何涉及标签第一个字符的字体的大小。任何其他字体和任何段落样式都被忽略。作为垂直体系空间约束的替代方案,STULabel提供了允许您创建基于涉及
STULabel
视图的确切行高的约束的NSLayoutYAxisAnchor
扩展方法,请参阅NSLayoutAnchor+STULabelSpacing.overlay.swift
或NSLayoutAnchor+STULabelSpacing.h
。 -
在iOS 9上直接使用
NSLayoutConstraint.init
创建基线约束,如果其涉及STULabel
视图,则可能不会工作。您可以借助布局锚点创建约束来解决这一限制。iOS 10及以后的iOS版本没有这个问题,因为NSLayoutConstraint
初始化程序会自动检索相应的布局锚点。
行高
-
当计算行高和间距时,《UILabel》忽略了字体
leading
(“行间距”)属性,而《UITextView》和《STULabel》则不会这么做。如果一个字体有正的leading
属性,这就可能导致默认行高和布局边界产生不一致。如果没有通过如合适的
lineSpacing
段落样式属性来补偿,忽略正的leading
通常会导致行间距不足,尤其是在对如阿拉伯语或泰语进行排版时。注意,虽然默认的label字体和由UIFont.systemFont
返回的字体有零的leading
,但由UIFont.preferredFont
返回的字体通常有正的leading
。(一些首选字体也有负的leading
,比如大小类别≤'large'时的'caption2'样式字体,但STULabel目前忽略了负的leading)。 -
UILabel
和UITextView
仅根据原始字体的排版度量来计算行高,而《STULabel》在默认文本布局模式下会考虑在排版过程中为原始字体替换的备用字体的度量。如果没有通过段落样式进行调整,忽略替换字体的度量通常会导致使用系统字体排版亚洲语言文本时的行高不足。如果您希望使用Text Kit的行为,可以将
STULabel.textLayoutMode
设置为.textKit
。(当前Text Kit的行为可能是为什么由
UIFont.preferredFont
返回的字体的高度和深度度量依赖于应用程序的区域设置,例如在泰语区域即使只显示英文文本,字体的高度和深度也可能更大。) -
STULabel.textLayoutMode
还会以其他方式影响行高和基线的精确位置,如STUTextLayoutMode
的文档中所述。如果您希望使用Text Kit的行为,请将textLayoutMode
设置为.textKit
。 -
如果《UILabel》只有一行文本,段落样式的行间距会被添加到内容的下方。如果一个标签有超过一行文本,它就不会在最后一行后面添加任何行间距。《STULabel》和《UITextView》不会模仿这种不一致性,并且永远不会在最后一行后面添加任何段落样式的行间距。
显示缩放四舍五入
当Core Graphics将非emoji符号绘制到位图上下文时,它将向上取整垂直符号位置(假设原点在左上角),使得基线Y坐标落在像素边界上,除非文本已旋转或上下文已配置为允许通过显式设置setShouldSubpixelPositionFonts(true)
和setShouldSubpixelQuantizeFonts(false)
来允许垂直子像素定位。(CGContext
的确切字体渲染行为完全未记录,也没有用于读取CGContext
当前配置的公共API函数。)
《UILabel》、《UITextView》和《STULabel》首先在忽略任何显示缩放四舍五入的情况下计算文本布局。
当UILabel
绘制文本时,它会调整绘制文本矩形的坐标原点,使得最后一个基线的Y坐标四舍五入到最近的像素边界。因此,第一个基线的确切位置取决于最后一个基线位置。
UITextView
和STULabel
不调整垂直文本位置,就像UILabel
做的那样。
UITextView
将基线显示的缩放四舍五入留给Core Graphics。
STULabel
预测Core Graphics的显示缩放四舍五入。当它绘制一行文本时,它会调整Core Graphics上下文中的文本矩阵,使基线落在下一个像素边界上。这种方法的优点是,渲染的emoji和非emoji符号总是具有正确的相对垂直对齐。
可能由于这些显示缩放四舍五入问题,UILabel
或UITextView
的内联内容高度或sizeThatFits
在特定情况下可能会短1个像素。同样,UILabel
或UITextView
基线锚点的垂直位置也可能偏离1个像素。STULabel
没有这些问题。
对齐和布局边界
-
UILabel
始终在其边界内垂直居中显示内容,而UITextView
始终使用顶部对齐。STULabel
允许您在顶部、底部和3种垂直居中对齐类型之间进行选择(围绕布局边界的中间点、x-height边界或cap-height边界)。 -
UITextView
和STULabel
都有可自定义的内容填充,即文本周围的填充。默认情况下,UITextView.textContainerInset
不为零,而STULabel.contentInsets
为零。STULabel
还支持根据UI布局方向设置填充(通过STULabel.directionalContentInsets
)并公开一个被固定到标签边界但无填充的UILayoutGuide
(STULabel.contentLayoutGuide
)。UILabel
没有内置的内容填充支持。可以通过子类化UILabel
并重写UILabel.textRect(forBounds:limitedToNumberOfLines:)
来实施此类填充,但这可能会破坏例如Auto Layout的支持部分,尤其是对于属性字符串。 -
内容填充对于确保超出图文布局边界的连字符或其他文本特征在渲染过程中不被剪切非常重要。由于
UILabel
没有填充,它使用不同的方法:当它显示文本时,它会根据文本内容和使用的字体计算文本的溢出量。这些溢出量通常是保守的,并基于文本是否包含来自某些Unicode范围的代码点。如果布局边界加上计算出的溢出量不适合标签视图边界,并且视图的clipsToBounds
属性为false(默认值),则文本将在超出轨位的子层上显示。如果没有这个功能,例如使用系统字体显示的阿拉伯语或泰语文本将会定期在UILabel
视图中被剪切。尽管
STULabel
支持内边距,但它不会依赖于内边距来确保变音符号和文本装饰不会被裁剪。与UILabel
类似,如果需要,它将切换到在子层中显示文本(除非将clipsContentToBounds
设置为true
)。然而,与UILabel
不同的是,STULabel
使用渲染文本的确切图像边界,而不是一些不准确的估计,来确定是否需要使用子层。这仅使性能开销增加几个百分点,因为STULabel
使用一个非常快的自定义符号边界缓存。
行分隔和截断
-
STULabel
选择的行分隔对于给定的布局宽度和字体大小可能与UILabel
和UITextView
选择的有所不同,尤其是对于复杂脚本。 -
当使用
STULabel.sizeThatFits(_ maxSize:)
方法计算适合指定大小的尺寸时,即使这意味着截断或缩放文本,也取决于截断和缩放设置。如果您需要一个不涉及截断的大小,请传递一个足够大的最大尺寸。此行为与UILabel
和UITextView
的行为不同。 -
与
UILabel
和UITextView
不同,STULabel
对不等于NSParagraphStyle.lineBreakMode.byWordWrapping
的NSParagraphStyle.lineBreakMode
的处理方式可能更加一致和直观。 -
与
UILabel
和UITextView
不同,STULabel
不允许在多段落截断中“头部”或“中部”截断第一段,因为这可能会误导读者关于被截断的文本部分。它将自动切换到“尾部”截断。类似地,如果段落本身完全适合,但下一行的任意单行不适合,则STULabel
将不会在段落后省略截断标记。 -
STULabel
不支持在段落级别的NSLineBreakMode.byClipping
,但您可以将.clip
作为STULabel.lastLineTruncationMode
指定,以允许在标签的末尾进行裁剪。 -
STULabel
不支持类似于UILabel
的“text tightening”,即负字距扩展,作为截断的替代方法,如通过allowsDefaultTighteningForTruncation
和NSParagraphStyle.allowsDefaultTighteningForTruncation
属性。如果您想避免截断,考虑通过指定小于 1 的
minimumTextScaleFactor
允许文本缩放。
文本属性和装饰
-
如果你将不带字体属性的文本范围设置为
STULabel
的attributedText
或shapedText
属性,Core Text 默认字体(Helvetica 12pt)将用于这些文本范围。由于这很可能不是你想要的字体,你应该确保所有属性字符串中的范围都有一个明确的字体。在这种情况下,UILabel
和UITextView
使用UIFont.systemFont(size: 17)
作为默认字体。 -
STULabel
通常在文本装饰上绘制与UILabel
和UITextView
略微不同。例如,下划线厚度基于原始字体和替换字体(这导致厚度更加一致)以及下降间距更加准确。 -
STULabel
不支持NSUnderlineStyle.byWord
。 -
STULabel
不支持.obliqueness
、.expansion
和.textEffect
字符串属性。在这些属性的替代方案中,你可以使用不同的字体(可能是具有非标准字体矩阵的字体)。 -
STULabel
不支持NSTextBlock
、NSTextList
、NSTextTable
和其他 Text Kit 属性。
链接
-
STULabel
没有像UITextView
那样通过dataDetectorTypes
属性自动检测 URL、电话号码等的内置支持。你不必让标签检测链接,可以编写一个辅助函数,该函数使用
NSDataDetector
来查找相关的文本范围,并构建包含适当链接的属性字符串。这种方式使得代码中执行此潜在的昂贵操作更加明朗,并将工作移入后台线程的过程简化。 -
STULabel
没有内置支持 3D Touch 或“Peek and Pop”链接预览功能。 -
文本中嵌入的链接在 Voice Over 中宣布的方式以及通过 Voice Over 可导航的链接在
STULabel
、UILabel
和UITextView
之间以及在不同的 iOS 版本中都不同。一些相关的UIAccessibility
API 是私有的,这使得STULabel
中的支持更加复杂。
其他限制
-
STULabel
当前不支持文本选择。(点映射到字符的基础设施已经存在,但还需要实现选择逻辑、手势识别等。) -
STULabel
和STUTextFrame
与UITextView
和NSTextContainer
不同,不支持指定一个排除路径。在某些情况下,水平段落缩进可能是一个足够替代排除路径的选项。
STUParagraphStyle
允许你指定段落中要应用initialLinesHeadIndent
和initialLinesTailIndent
的行数(类似于 Android 的LeadingMarginSpan2
)这使得缩进比UILabel
和UITextView
中要灵活。 -
STULabel 不支持垂直文本。
(此列表并不完整。)