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”)软件。它存在bug,需要更多测试和文档,但仍可能已经足够满足您的要求。如果您希望将其用于任何严肃的用途,请订阅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。
- 如果您想使用静态框架,需要将产品
STULabelResources.bundle
手动添加到您的应用程序或框架目标中。(资源包包含默认链接操作表的本地化字符串。) - 将STULabel手动集成到您的Xcode项目中的方法如下
-
如果它是打开的,请在Xcode中关闭STULabel项目。
-
如果您的项目尚未打开,请在Xcode中打开它。
-
将STULabel项目从Finder拖到Xcode项目窗口的项目导航器面板中。
-
在项目导航器中展开
STULabel.xcodeproj
下方的子树,并展开Products
组中的项。现在,您应该会看到两个STULabel.framework
项、两个STULabelSwift.framework
项、一个STULabelResources.bundle
和一些其他项。同名框架项是各自框架的动态和静态构建。您可以通过Xcode文件检查器(在右边的Xcode侧窗格中)中的完整路径来识别静态框架。例如,静态STULabel.framework
的完整路径以'-static/STULabel.framework
'结尾。 -
在Xcode项目导航器面板的顶部选择您的项目。
-
在中心视图中选择您的应用程序或框架目标。
-
在中心视图中选择“通用”选项卡。
-
如果您想使用动态框架
- 将
STULabel.xcodeproj
中的Products
组中的非静态STULabel.framework
拖到中心视图中“嵌入式二进制文件”部分。当您将其放下时,该框架也将添加到下面的“链接的框架和库”列表中。 - 如果想要使用Swift,则以相同的方式使用非静态
STULabelSwift.framework
。
如果您想使用静态框架则代替
- 将以下项添加到“链接的框架和库”部分,例如,通过单击“+”按钮并选择相应的项
- 从静态目标中
STULabel.framework
- 如果想要使用Swift,则从静态目标中选择
STULabelSwift.framework
- 除非该库之前已经添加,否则添加
libc++.tbd
- 从静态目标中
- 在中心视图中选择“构建阶段”选项卡。
- 将您刚刚添加的静态
STULabel(Swift)
框架也添加到“链接的框架”列表中。 - 将
STULabelResources
添加到“目标依赖”列表中。 - 展开“复制资源包资源”部分。
- 将
STULabelResources.bundle
从Products
组中的STULabel.xcodeproj
拖到“复制资源包资源”部分。 - 在中心视图中选择“构建设置”选项卡。
- 将
$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-static
添加到“框架搜索路径”。
- 将
-
LLDB格式化工具
STULabel源代码包含一个LLDB Python 脚本,用于定义由库定义的各种类型的数据格式。如果在Xcode中逐行读取STULabel代码,导入此脚本将提高您的调试体验。
支持
如果您发现了一个错误,请创建一个GitHub问题。如有可能,请提供重现问题的示例代码。
如果您关于STULabel的使用有任何疑问,例如如何实现某个特定的文本布局,请在Stack Overflow上提问并标记为'STULabel'。
某些功能的详细说明
性能
使用STULabel
进行同步布局和渲染比UILabel
和UITextView
快,有时甚至快几倍。STULabel
到底快多少,既取决于特定的用例,也取决于设备和iOS的版本。Demo应用程序包含了一个用于标签视图的微基准测试,可以让学生在自述文件上比较STULabel
、UILabel
和UITextView
在多种测试案例上的性能。
STULabel
比UILabel
快,主要是因为它更积极地缓存文本布局数据。部分原因是由于UILabel
使用NSStringDrawing
进行布局和渲染,它不支持保留计算出的文本布局,而STULabel
使用的是STUTextFrame
API(建立在Core Text的CTTypesetter
之上),这使得将文本形状和布局从文本渲染中分离出来变得非常简单。
看起来UITextView
主要是设计用于懒惰地设置大型可变文本以及支持对布局过程的精细定制,而不是用于显示小型静态字符串。
STULabel
中的自动文本缩放实现特别快,因为它不是缩小字体大小,而是在绘制过程中先扩大布局宽度,然后缩小内容。这具有优点,即不需要重新创建属性字符串,字体对象缓存较小,大部分文本形状只需要做一次。iOS上Core Graphics的渲染质量在这两种方法上应该是一样的。
异步渲染
文本布局和渲染可以构成主线程在布局和渲染上花费的大量时间,尤其是当文本使用复杂的脚本编写时,例如例如印地语或阿拉伯语。在STULabel
中,异步布局和渲染支持可以让您轻松地将至少部分工作移至后台线程。(
演示应用程序中的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
在 IB 中像 UILabel
或 UITextView
那样工作。
如果您可以忍受 IBInspectable
的限制,例如,因为您的应用程序只使用一组固定的“样式”,那么当然可以子类化 STULabel
并使其子类 IBDesignable
。
对 Auto Layout 的支持
UIKit 中对于 UILabel
和 UITextView
的 Auto Layout 支持广泛地使用了私有 API。因此,STULabel 的 Auto Layout 支持有一定的限制。
-
自动布局本身不支持内容高度(非线性)依赖于布局宽度的视图。为了在自动布局中支持多行文本视图,UIKit提供了一项私有API,允许
UILabel
和UITextView
选择参与特殊的两步布局过程。在复杂情况下,这项未记录的两步布局有时结果不尽如人意。STULabel
不能参与两步布局,它采用不同的方法:通过计算在无限宽度和当前视图宽度下文本的大小,然后从这两个尺寸中取最长宽度和高度,来计算固有内容大小。当视图宽度改变时,固有内容大小会被标记为无效,并迫使UIKit布局算法更新,以满足变化后的固有内容大小。这种方法似乎即使在复杂情况下也能可靠地工作。 -
UIView.systemLayoutsSizeFitting(...)
方法纯根据子视图约束来计算视图的大小。它们不会调用任何layoutSubviews
方法,因此如果子视图层次结构中的任何视图依赖于手动布局代码,则通常无法确定正确的视图大小。由于STULabel
的自动布局支持依赖于完整的UIKit布局过程,包括调用layoutSubviews
,因此如果包含多行的STULabel
子视图的视图已经具有正确的宽度,则systemLayoutsSizeFitting
不会计算包含多行STULabel
子视图的视图的正确大小。(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
或firstBaselineLayout
属性返回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中,如果涉及到
STULabel
视图,直接使用NSLayoutConstraint.init
创建基线约束将无法正常工作。您可以通过借助布局锚点创建约束来解决这个问题。iOS 10及以后的iOS版本没有这个问题,因为NSLayoutConstraint
初始化器会自动检索相应的布局锚点。
行高
-
UILabel
在计算行高和间距时忽略了字体leading
(行间距)属性,而UITextView
和STULabel
则不会。如果字体具有正的leading
,这会导致默认行高和布局边界出现差异。除非通过例如适当的
lineSpacing
段落样式属性进行补偿,否则忽略正的leading通常会导致行间距不足,尤其是在排版例如阿拉伯语或泰语文本时。请注意,虽然默认的标签字体和由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将非表情符号符号绘制到位图上下文时,它将垂直符号位置向上取整(假设以左上角为起点)使得基线Y坐标落在像素边界上,除非文本被旋转或上下文已被配置为允许通过显式设置两个setShouldSubpixelPositionFonts(true)
和setShouldSubpixelQuantizeFonts(false)
来允许垂直子像素定位。(Core Graphics和Core Text的精确字体渲染行为完全未经文档记录,且没有公开的API函数可以用于读取CGContext
的当前配置。)
UILabel
、UITextView
和STULabel
都将首先计算文本布局,忽略任何显示比例取整。
当UILabel
绘制文本时,它会调整绘制文本矩形的起点,使得最后基线的Y坐标向上取整到最近的像素边界。因此,第一基线的确切位置取决于最后基线的位置。
UITextView
和STULabel
不会像UILabel
那样调整垂直文本位置。
UITextView
将基线显示比例取整的问题留给Core Graphics处理。
STULabel
预测Core Graphics的显示比例取整。当它绘制一行文本时,它调整Core Graphics上下文的文本矩阵,使得基线落在下一个像素边界上。这种方法的优势在于渲染的表情符号和非表情符号符号始终有正确的垂直对齐关系。
可能由于这些显示比例取整问题,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
的行为不同。 -
STULabel
、UILabel
和UITextView
都处理了不等于byWordWrapping
的NSParagraphStyle.lineBreakMode
,但STULabel
的行为无疑是连贯且直观的。 -
与
UILabel
和UITextView
相反,STULabel
不允许在多段落截断中“头部”或“中间”截断第一个段落,因为这可能会误导读者关于被截断的文本部分。它将自动切换到“尾部”截断。同样,如果段落本身完全适合但下一行文本的任何一行都不适合,STULabel
将不会在段落后省略截断标记。 -
STULabel
不支持在段落级别使用NSLineBreakMode.byClipping
,但您可以指定.clip
作为STULabel.lastLineTruncationMode
,以允许在标签末尾进行裁剪。 -
STULabel
不支持像UILabel
通过allowsDefaultTighteningForTruncation
和NSParagraphStyle.allowsDefaultTighteningForTruncation
属性那样作为截断的替代方案使用“文本紧缩”,即负字间距。如果您想避免截断,考虑指定小于1的
minimumTextScaleFactor
以允许文本缩放。
文本属性和装饰
-
如果您将包含文本范围但不包含字体属性的属性字符串设置为
STULabel
的attributedText
或shapedText
属性,那么Core Text默认字体(Helvetica 12pt)将用于这些文本范围。鉴于这很可能是您不想要的字体,因此应确保属性字符串中所有范围都具有显式指定的字体。UILabel
和UITextView
在这种情况下使用UIFont.systemFont(size: 17)
作为默认字体。 -
STULabel
通常以与UILabel
和UITextView
不同的方式绘制文本装饰。例如,下划线厚度是根据原始字体和替换字体(导致_thickness_更一致)以及基线间距更准确来计算的。 -
STULabel
不支持NSUnderlineStyle.byWord
。 -
STULabel
不支持.obliqueness
、.expansion
和.textEffect
字符串属性。相反,您可以使用不同的字体(可能是一个具有非标准字体矩阵的字体)。 -
STULabel
不支持NSTextBlock
、NSTextList
、NSTextTable
和其他Text Kit属性。
链接
-
STULabel
没有内置的自动检测URL、电话号码等功能的支持,就像UITextView
通过dataDetectorTypes
属性做的那样。除了让标签检测链接外,您可以实现一个辅助函数,该函数使用
NSDataDetector
查找相关的文本范围并构造包含适当链接的属性字符串。通过这种方式执行,可以使代码中潜在昂贵的操作运行变得明显,并简化将工作移动到后台线程。 -
STULabel
没有内置的3D触摸或"查看和弹出"链接预览支持。 -
文本中嵌入的链接由Voice Over宣布的方式以及通过Voice Over可导航的链接的方式在
STULabel
、UILabel
和UITextView
之间以及iOS版本之间不同。一些相关的UIAccessibility
API是私有的,这使得在STULabel
中的支持变得复杂。
其他限制
-
STULabel
当前不支持文本选择。(映射点到字符的基础结构已经存在,但还需要实现选择逻辑、手势识别等。) -
STULabel
和STUTextFrame
不支持指定排除路径,与UITextView
和不同。
在某些情况下,水平段落缩进可能是一个足够的替代方案来指定一般排除路径。
STUParagraphStyle
允许您指定初始行头缩进initialLinesHeadIndent
和初始行尾缩进initialLinesTailIndent
适用于段落中的行数(类似于您可以使用 Android 的LeadingMarginSpan2
做到的),使缩进比UILabel
和UITextView
更灵活。 -
STULabel 不支持垂直文本。
(此列表不完整。)