STULabel 0.8.11

STULabel 0.8.11

Stephan Tolksdorf 维护。



STULabel 0.8.11

CircleCI TravisCI Swift 4.2 Version Platform License Twitter

STULabel 是一个开源的 iOS 框架,使用 Swift 和 Objective-C 实现,提供了一个标签视图(STULabel),一个标签层(STULabelLayer)和一个灵活的 API 用于线程安全的文本布局和渲染(STUShapedStringSTUTextFrame)。这个框架是基于 Core Text API 的底层部分用 Objective-C++ 实现的。STULabel 具有一个 Swift 视图层(STULabelSwift),它提供了一个方便的 Swift API。

目录

STULabel 特点

  • UILabelUITextView 更快
  • 可选的异步布局和渲染
  • 支持预渲染(例如,在集合视图预取处理程序中很有用)
  • 自动切换到高效拼图渲染以处理非常大的标签
  • 使用颜色或装饰进行文本突出显示,而无需重新布局文本
  • 快速文本自动缩放(“适应大小”)
  • 支持完整的 UIDragInteraction 的交互式超链接
  • 非常灵活的文本截断,包括对包含链接的截断令牌和多个垂直堆叠截断范围的支持
  • 支持自动布局
  • 支持动态类型
  • 支持 UIAccessibility
  • 全面支持从右到左的文本
  • 可配置的垂直对齐和内容内边距
  • 对文本布局的精细控制,包括对固定基线距离和首行偏移量的支持
  • 可定制的自动连字符分离
  • 文本附件(内联图片)
  • 带有精确下沉间隙的下划线
  • 灵活的背景装饰,例如带有圆角的装饰
  • 易于查询的丰富文本布局信息

源代码包含一个演示应用程序,您可以使用包含的 Xcode 项目构建它。演示应用程序包含

  • 一款适用于《世界人权宣言》的查看器,支持以39种不同书写系统查看文档。您可以尝试调整字体、间距、文本装饰、链接、截断等功能,并将由 STULabel 渲染的文本与由 UITextView 渲染的文本进行对比。
  • 一款对 UITableView 进行滚动的压力测试,可让您比较 STULabelUILabelUITextView 的性能,并观察启用或禁用自动布局、异步渲染或预取布局/渲染对性能的影响。
  • 一款微基准测试,可让您针对各种测试用例衡量和比较 STULabelUILabelUITextView 的布局和渲染性能。
  • 一款微基准测试,可让您针对各种测试用例衡量和比较 STUTextFrameNSStringDrawing 和 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项目中的方法如下
    1. 如果它是打开的,请在Xcode中关闭STULabel项目。

    2. 如果您的项目尚未打开,请在Xcode中打开它。

    3. 将STULabel项目从Finder拖到Xcode项目窗口的项目导航器面板中。

    4. 在项目导航器中展开STULabel.xcodeproj下方的子树,并展开Products组中的项。现在,您应该会看到两个STULabel.framework项、两个STULabelSwift.framework项、一个STULabelResources.bundle和一些其他项。同名框架项是各自框架的动态和静态构建。您可以通过Xcode文件检查器(在右边的Xcode侧窗格中)中的完整路径来识别静态框架。例如,静态STULabel.framework的完整路径以' -static/STULabel.framework'结尾。

    5. 在Xcode项目导航器面板的顶部选择您的项目。

    6. 在中心视图中选择您的应用程序或框架目标。

    7. 在中心视图中选择“通用”选项卡。

    8. 如果您想使用动态框架

      • STULabel.xcodeproj中的Products组中的非静态 STULabel.framework拖到中心视图中“嵌入式二进制文件”部分。当您将其放下时,该框架也将添加到下面的“链接的框架和库”列表中。
      • 如果想要使用Swift,则以相同的方式使用非静态 STULabelSwift.framework

      如果您想使用静态框架则代替

      • 将以下项添加到“链接的框架和库”部分,例如,通过单击“+”按钮并选择相应的项
        • 从静态目标中STULabel.framework
        • 如果想要使用Swift,则从静态目标中选择STULabelSwift.framework
        • 除非该库之前已经添加,否则添加libc++.tbd
      • 在中心视图中选择“构建阶段”选项卡。
      • 将您刚刚添加的静态STULabel(Swift)框架也添加到“链接的框架”列表中。
      • STULabelResources添加到“目标依赖”列表中。
      • 展开“复制资源包资源”部分。
      • STULabelResources.bundleProducts组中的STULabel.xcodeproj拖到“复制资源包资源”部分。
      • 在中心视图中选择“构建设置”选项卡。
      • $(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)-static添加到“框架搜索路径”。

LLDB格式化工具

STULabel源代码包含一个LLDB Python 脚本,用于定义由库定义的各种类型的数据格式。如果在Xcode中逐行读取STULabel代码,导入此脚本将提高您的调试体验。

支持

如果您发现了一个错误,请创建一个GitHub问题。如有可能,请提供重现问题的示例代码。

如果您关于STULabel的使用有任何疑问,例如如何实现某个特定的文本布局,请在Stack Overflow上提问并标记为'STULabel'。

某些功能的详细说明

性能

使用STULabel进行同步布局和渲染比UILabelUITextView快,有时甚至快几倍STULabel到底快多少,既取决于特定的用例,也取决于设备和iOS的版本。Demo应用程序包含了一个用于标签视图的微基准测试,可以让学生在自述文件上比较STULabelUILabelUITextView在多种测试案例上的性能。

STULabelUILabel快,主要是因为它更积极地缓存文本布局数据。部分原因是由于UILabel使用NSStringDrawing进行布局和渲染,它不支持保留计算出的文本布局,而STULabel使用的是STUTextFrame API(建立在Core Text的CTTypesetter之上),这使得将文本形状和布局从文本渲染中分离出来变得非常简单。

看起来UITextView主要是设计用于懒惰地设置大型可变文本以及支持对布局过程的精细定制,而不是用于显示小型静态字符串。

STULabel中的自动文本缩放实现特别快,因为它不是缩小字体大小,而是在绘制过程中先扩大布局宽度,然后缩小内容。这具有优点,即不需要重新创建属性字符串,字体对象缓存较小,大部分文本形状只需要做一次。iOS上Core Graphics的渲染质量在这两种方法上应该是一样的。

异步渲染

文本布局和渲染可以构成主线程在布局和渲染上花费的大量时间,尤其是当文本使用复杂的脚本编写时,例如例如印地语或阿拉伯语。在STULabel中,异步布局和渲染支持可以让您轻松地将至少部分工作移至后台线程。(Image decoding and drawing

演示应用程序中的UITableView示例允许您启用或禁用STULabel视图的异步和预取渲染,从而让您可以观察这些功能对滚动性能的影响。(Fast device

您可以通过将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 不支持 UIFontNSAttributedStringUIEdgeInsets 或任何枚举类型作为 IBInspectable 属性的类型,因此目前没有任何方法使 STULabel 在 IB 中像 UILabelUITextView 那样工作。

如果您可以忍受 IBInspectable 的限制,例如,因为您的应用程序只使用一组固定的“样式”,那么当然可以子类化 STULabel 并使其子类 IBDesignable

对 Auto Layout 的支持

UIKit 中对于 UILabelUITextView 的 Auto Layout 支持广泛地使用了私有 API。因此,STULabel 的 Auto Layout 支持有一定的限制。

  • 自动布局本身不支持内容高度(非线性)依赖于布局宽度的视图。为了在自动布局中支持多行文本视图,UIKit提供了一项私有API,允许UILabelUITextView选择参与特殊的两步布局过程。在复杂情况下,这项未记录的两步布局有时结果不尽如人意。

    STULabel不能参与两步布局,它采用不同的方法:通过计算在无限宽度和当前视图宽度下文本的大小,然后从这两个尺寸中取最长宽度和高度,来计算固有内容大小。当视图宽度改变时,固有内容大小会被标记为无效,并迫使UIKit布局算法更新,以满足变化后的固有内容大小。这种方法似乎即使在复杂情况下也能可靠地工作。

  • UIView.systemLayoutsSizeFitting(...)方法纯根据子视图约束来计算视图的大小。它们不会调用任何layoutSubviews方法,因此如果子视图层次结构中的任何视图依赖于手动布局代码,则通常无法确定正确的视图大小。由于STULabel的自动布局支持依赖于完整的UIKit布局过程,包括调用layoutSubviews,因此如果包含多行的STULabel子视图的视图已经具有正确的宽度,则systemLayoutsSizeFitting不会计算包含多行STULabel子视图的视图的正确大小。(UILabelUITextView没有这个问题,因为它们得到了之前提到的特殊两步布局处理。)

    如果您在自己的代码中调用systemLayoutsSizeFitting,您可能可以用其上级视图上的layoutIfNeeded调用(如果需要,可以通过将其临时添加为一个上级视图的孩子来实现)来替换它。

    UITableViewUICollectionView使用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的限制,从viewForFirstBaselineLayoutfirstBaselineLayout属性返回STULabel将不会产生期望的效果。但是,如果您覆盖firstBaselineAnchorlastBaselineAnchor并将相应的锚点从标签子视图中传递下去,基线约束应该按预期工作。

  • 在iOS 11中引入的系统间距约束,例如通过constraint(equalToSystemSpacingBelow:multiplier:)创建的,不会与STULabel视图正常工作,因为它们依赖于私有UIKit API。这些约束的确切行为尚未记录。iOS 12的实现只会根据所涉及任何标签的第一个字符的字体来计算间距。任何其他字体和任何段落样式都被忽略。

    作为垂直系统间距约束的替代方案,STULabel提供了允许您创建相对于涉及的STULabel视图的确切行高的约束的NSLayoutYAxisAnchor扩展方法,请参阅NSLayoutAnchor+STULabelSpacing.overlay.swiftNSLayoutAnchor+STULabelSpacing.h

  • 在iOS 9中,如果涉及到STULabel视图,直接使用NSLayoutConstraint.init创建基线约束将无法正常工作。您可以通过借助布局锚点创建约束来解决这个问题。iOS 10及以后的iOS版本没有这个问题,因为NSLayoutConstraint初始化器会自动检索相应的布局锚点。

行高

  • UILabel在计算行高和间距时忽略了字体leading(行间距)属性,而UITextViewSTULabel则不会。如果字体具有正的leading,这会导致默认行高和布局边界出现差异。

    除非通过例如适当的lineSpacing段落样式属性进行补偿,否则忽略正的leading通常会导致行间距不足,尤其是在排版例如阿拉伯语或泰语文本时。请注意,虽然默认的标签字体和由UIFont.systemFont返回的字体具有零leading,但由UIFont.preferredFont返回的字体通常具有正的leading。(一些首选字体也有负的leading,例如大小类别≤'large'的'caption2'样式的字体,但STULabel目前忽略了负leading。)

  • UILabelUITextView仅根据原始字体的印刷度量计算行高,而STULabel在默认文本布局模式下还将考虑在排版过程中为原始字体替代的备用字体的度量。

    除非通过段落样式进行补偿,否则在排版例如使用系统字体的亚洲语言文本时,忽略替代字体的度量通常会导致行高不足。如果您仍然希望使用Text Kit的行为,您可以设置STULabel.textLayoutMode.textKit

    (当前的Text Kit行为可能是为什么UIFont.preferredFont返回的字体的高度和深度度量取决于应用程序的区域设置,例如在泰语区域即使只显示英语文本,度量也会更大。)

  • STULabel.textLayoutMode还会以其他方式影响其他方面,如STUTextLayoutMode文档所述的准确行高和基线位置。如果您希望使用Text Kit的行为,请将textLayoutMode设置为.textKit

  • 如果UILabel只有单行,段落样式的行间距会被添加到内容的底部。如果标签有多行,它不会在最后一行之后添加任何行间距。STULabelUITextView不会模仿这种不一致,且永远不会在最后一行之后添加任何段落样式行间距。

显示缩放舍入

当Core Graphics将非表情符号符号绘制到位图上下文时,它将垂直符号位置向上取整(假设以左上角为起点)使得基线Y坐标落在像素边界上,除非文本被旋转或上下文已被配置为允许通过显式设置两个setShouldSubpixelPositionFonts(true)setShouldSubpixelQuantizeFonts(false)来允许垂直子像素定位。(Core Graphics和Core Text的精确字体渲染行为完全未经文档记录,且没有公开的API函数可以用于读取CGContext的当前配置。)

UILabelUITextViewSTULabel都将首先计算文本布局,忽略任何显示比例取整。

UILabel绘制文本时,它会调整绘制文本矩形的起点,使得最后基线的Y坐标向上取整到最近的像素边界。因此,第一基线的确切位置取决于最后基线的位置。

UITextViewSTULabel不会像UILabel那样调整垂直文本位置。

UITextView将基线显示比例取整的问题留给Core Graphics处理。

STULabel预测Core Graphics的显示比例取整。当它绘制一行文本时,它调整Core Graphics上下文的文本矩阵,使得基线落在下一个像素边界上。这种方法的优势在于渲染的表情符号和非表情符号符号始终有正确的垂直对齐关系。

可能由于这些显示比例取整问题,UILabelUITextView的内禀内容高度或sizeThatFits在某种情况下可能会短1像素。同样,UILabelUITextView基线锚点的垂直位置可能偏离1像素。STULabel没有这些问题。

对齐和布局边界

  • UILabel始终在其边界内垂直居中显示内容,而UITextView始终使用顶部对齐。STULabel让您可以选择在顶部、底部和3种垂直居中对齐之间(围绕布局边界的中间、x-height边界或cap-height边界)进行选择。

  • UITextViewSTULabel都允许自定义内容内边距,即文本周围的填充。默认情况下,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针对特定布局宽度和字体大小选择的行断裂可能略不同于UILabelUITextView,特别是在复杂的脚本中。

  • STULabel.sizeThatFits(_ maxSize:)方法将计算适合指定大小的大小,即使这意味着截断或缩放文本,如截断和缩放设置所指定。如果您希望得到不涉及截断的大小,请传递足够大的最大大小。这种行为与UILabelUITextView的行为不同。

  • STULabelUILabelUITextView都处理了不等于byWordWrappingNSParagraphStyle.lineBreakMode,但STULabel的行为无疑是连贯且直观的。

  • UILabelUITextView相反,STULabel不允许在多段落截断中“头部”或“中间”截断第一个段落,因为这可能会误导读者关于被截断的文本部分。它将自动切换到“尾部”截断。同样,如果段落本身完全适合但下一行文本的任何一行都不适合,STULabel将不会在段落后省略截断标记。

  • STULabel不支持在段落级别使用NSLineBreakMode.byClipping,但您可以指定.clip作为STULabel.lastLineTruncationMode,以允许在标签末尾进行裁剪。

  • STULabel不支持像UILabel通过allowsDefaultTighteningForTruncationNSParagraphStyle.allowsDefaultTighteningForTruncation属性那样作为截断的替代方案使用“文本紧缩”,即负字间距。

    如果您想避免截断,考虑指定小于1的minimumTextScaleFactor以允许文本缩放。

文本属性和装饰

  • 如果您将包含文本范围但不包含字体属性的属性字符串设置为STULabelattributedTextshapedText属性,那么Core Text默认字体(Helvetica 12pt)将用于这些文本范围。鉴于这很可能是您不想要的字体,因此应确保属性字符串中所有范围都具有显式指定的字体。UILabelUITextView在这种情况下使用UIFont.systemFont(size: 17)作为默认字体。

  • STULabel通常以与UILabelUITextView不同的方式绘制文本装饰。例如,下划线厚度是根据原始字体和替换字体(导致_thickness_更一致)以及基线间距更准确来计算的。

  • STULabel不支持NSUnderlineStyle.byWord

  • STULabel不支持.obliqueness.expansion.textEffect字符串属性。相反,您可以使用不同的字体(可能是一个具有非标准字体矩阵的字体)。

  • STULabel不支持NSTextBlockNSTextListNSTextTable和其他Text Kit属性。

链接

  • STULabel没有内置的自动检测URL、电话号码等功能的支持,就像UITextView通过dataDetectorTypes属性做的那样。

    除了让标签检测链接外,您可以实现一个辅助函数,该函数使用NSDataDetector查找相关的文本范围并构造包含适当链接的属性字符串。通过这种方式执行,可以使代码中潜在昂贵的操作运行变得明显,并简化将工作移动到后台线程。

  • STULabel没有内置的3D触摸或"查看和弹出"链接预览支持。

  • 文本中嵌入的链接由Voice Over宣布的方式以及通过Voice Over可导航的链接的方式在STULabelUILabelUITextView之间以及iOS版本之间不同。一些相关的UIAccessibility API是私有的,这使得在STULabel中的支持变得复杂。

其他限制

  • STULabel当前不支持文本选择。(映射点到字符的基础结构已经存在,但还需要实现选择逻辑、手势识别等。)

  • STULabelSTUTextFrame 不支持指定排除路径,与 UITextView 不同。

    在某些情况下,水平段落缩进可能是一个足够的替代方案来指定一般排除路径。 STUParagraphStyle 允许您指定初始行头缩进 initialLinesHeadIndent 和初始行尾缩进 initialLinesTailIndent 适用于段落中的行数(类似于您可以使用 Android 的 LeadingMarginSpan2 做到的),使缩进比 UILabelUITextView 更灵活。

  • STULabel 不支持垂直文本。

(此列表不完整。)