大多数浏览器和
Developer App 均支持流媒体播放。
-
TextKit 和文本视图的新功能
了解 UI 框架的 TextKit 和文本视图的最新更新。探索布局优化和 API 增强,学习如何在多个操作系统版本之间保持兼容性,以及确定利用 TextKit 2 对您的 App 进行现代化的方法。为能更好地理解此讲座,请先观看 WWDC21 的“认识 TextKit 2”。
资源
相关视频
WWDC22
WWDC21
-
下载
大家好 欢迎收看 “TextKit 和文本视图的新功能” 我叫 Donna Tom 是一名 TextKit 工程师
在 iOS 15 和 macOS Monterey 中 我们引入了 TextKit 2 这是一个功能强大的新文本引擎 具有更高的性能 正确性和安全性
TextKit 2 基于视口的布局架构 提供了高性能的文本布局 尤其是对于包含大量内容的文档
TextKit 2 通过消除使用字形 不必要的复杂性 为全球的用户提供了 更好的文本体验 并且它完全支持 OpenType 和可变字体等现代字体技术
TextKit 2 专注于使用更高级的对象 来控制文本布局 使您可以更轻松地自定义文本的布局 从而可以用更少的代码 来构建更酷的东西
接下来 TextKit 2 引擎构成了 所有 Apple 平台上 文本布局和渲染的基础
未来的性能增强 更新和改进 都将集中在 TextKit 2 引擎上 更新到 TextKit 2 后 您的 App 将可以 在我们推出这些改进后 从中受益 有关 TextKit2 的深入介绍 请观看 Meet TextKit2 视频 该视频涵盖了基础知识 以及如何使用 TextKit 2 构建自己的文本布局组件 另一方面 本视频则会介绍 TextKit 2 的最新进展 以及如何充分利用 TextKit 2 支持的多个文本视图 没错 我说的是多个文本视图 复数形式 因为现在 截至 iOS 16 和 macOS Ventura UIKit 和 AppKit 中的 所有文本控件都使用 TextKit 2 包括 UITextView 所以我们在整个系统中使用 TextKit 2 进行布局和渲染 让所有 App 尽快过渡到 TextKit 2 非常重要 我们添加了许多工具 来帮您更轻松地过渡 对于许多 App 来说 这可能是零代码转换 我们预计 对于那些没有对文本视图 进行任何特殊修改的 App 来说 情况也是如此 稍后我会告诉您更多这方面的信息
但首先 我将介绍 TextKit 2 中的新增功能 包括我刚才提到的一些工具
之后 我将深入讲解文本视图的 TextKit 1 兼容模式的细节
然后我将讨论在准备将代码 转换到 TextKit 2 时 可以使用的现代化策略
首先是 TextKit 2 中的新功能
TextKit 2 最早出现在 iOS 15 中的 UIKit 为了使用 TextKit 2 UITextField 进行了升级 在 iOS 16 中 UIKit 向 TextKit 2 的过渡已经完成 所有文本控件默认使用 TextKit 2 包括 UITextView 大多数文本视图 将自动选择加入 TextKit 2 而无需您的任何操作 只有少数情况下 文本视图可能无法选择 我将在本视频的兼容性部分进行介绍
AppKit 也有类似的情况 TextKit 2 最早出现在 macOS Big Sur 中的 AppKit 在 macOS Monterey 中 NSTextField 已升级为默认使用它 通过选择加入 它可用于 NSTextView
在 macOS Ventura 中 所有文本控件默认使用 TextKit 2 就像 UITextView 大多数 NSTextViews 自动选择加入TextKit 2 而不需要您的任何操作
TextEdit 是 NSTextView 的 一个薄包装器 它在 macOS Ventura 的任何地方 都使用 TextKit 2 自 macOS Big Sur 以来 TextEdit 一直在纯文本模式下使用 TextKit 2 在 macOS Ventura 中 富文本模式也使用 TextKit 2
由于 TextKit 2 是新标准 我们为 UITextView 和 NSTextView 添加了一些方便的构造函数 使用这些新的构造函数在初始化时 选择使用哪个文本引擎
要创建使用 TextKit 2 的文本视图 请使用新的构造函数 并为 UsingTextLayoutManager 参数传递 true 如果文本视图需要使用 TextKit 1 以实现兼容性 则改为传递 false
在 Interface Builder 中创建的 文本视图有一个新的文本布局选项 通过此新选项 您可以控制在每个实例上 使用哪种布局系统 默认设置是系统默认值 即 TextKit 2
还可以选择显式使用 TextKit 2 或 TextKit 1
TextKit 2 现在支持非简单文本容器 非简单文本容器中可能有漏洞或缺口 这允许文本环绕图像或其他内联内容
若要创建非简单文本容器 请使用 NSTextContainer 上的 exclusionPaths 属性来定义 不应放置文本的区域 关于如何做到这一点的示例 请查看与此视频相关的参考资料中的 TextKitAndTextView 示例代码 您可以在排除路径选项卡上 找到相关示例
我们增强了 TextKit 2 中的 换行符引擎 为两端对齐的段落选择 更均匀的换行符 这是一个微妙的变化 在较长的文本段落中更容易注意到
在这里我们有两个版本的同一文本 放在同一个区域
请注意 使用传统的换行符时 线条会拉伸 字间距也会变大
在新的偶数换行中 这种情况要少得多 这使得文本更容易阅读 并且您可以通过 TextKit 2 免费获得 无需采用
最后 我们在 TextKit 2 中 为所有平台添加了文本列表支持 使用文本列表 您可以通过编程方式 创建编号或项目符号列表 以便在文本视图中显示 TextKit 2 使用 NSTextList 来表示文本列表 就像 TextKit 1 一样 NSTextList 过去仅在 AppKit 中可用 但在iOS 16 中 它在 UIKit 中也可用
将 NSTextList 与 NSmutableParagraphStyle 一起使用 可以指定文本存储中的段落 格式化为列表以供显示 文本视图负责从文本存储中 提取这些属性 并将段落内容重新格式化为列表
虽然 NSTextList 本身并不是新的 但有一些新增的 TextKit 2 由于列表有嵌套项 所以很自然地将它们表示为树结构 在 TextKit 2 中 我们增强了 NSTextElement 以支持将它们结构化为具有 访问子元素和父元素属性的树
我们还添加了一个新的 元素子类 叫做NSTextListElement 当内容管理器在文本内容中 遇到 NSTextList 时 它将生成 NSTextListElements 来表示列表中的项目
要更深入地了解 如何创建文本列表和添加项目 请参阅 TextKitAndTextView 示例代码 您可以在列表选项卡上找到相关示例
在探索示例代码时 请不要错过文本附件示例 该示例演示了如何使用 TextKit 2 中的 文本附件视图提供程序 API
这些 API 允许您使用 UI 或 NSView 作为文本附件 并且事件可以由附件视图直接处理 因此使用文本附件处理事件 就变得更加容易了 而且只有使用 TextKit 2 才有可能做到这点 好了 这就是 TextKit 2 中新增的功能 接下来 我将详细介绍 TextKit 1 兼容性模式 由于 TextKit 2 与 TextKit 1 的设计 截然不同 我们理解 对于大量投资于 TextKit 1 架构的 App 来说 全面采用 TextKit 2 可能需要一些时间 我们希望这些 App 能够继续工作 直到实现过渡 这就是为什么我们为 UITextView 和 NSTextView 添加了一个特殊的 TextKit 1 兼容模式 当您显式调用 NSLayoutManager API 时 文本视图将用 NSTextLayoutManager 替换其 NSLayoutManager 并将自身重新配置为使用 TextKit 1 如果文本视图遇到 TextKit 2 尚不支持的属性 例如表格 或者在打印时 也会发生这种情况
如果在 UITextView 中 遇到意外运行时回退到 TextKit 1 请检查日志中有关开关的警告消息 在符号下划线上设置断点 以捕获堆栈跟踪 和其他有用的调试信息
对于 NSTextView 您可以通过订阅 willSwitch 或 didSwitchToNSLayoutManager 通知 来获取有关意外运行时回退的 更多信息
如果您必须退回到 TextKit 1 最好在初始化时退出 以编程方式初始化的文本视图 通过使用自己的文本容器和 TextKit 1 布局管理器来实现这一点
另一种选择是使用新的便利构造函数 来初始化 TextKit 1 文本视图 并将 false 作为参数传递 这将使您的文本视图使用 TextKit 1
第三个选项是使用 Interface Builder 并在文本视图中 将新的文本布局选项设置为 TextKit 1
这里有一点是需要注意的 如果要在初始化期间或之后 调出文本容器的布局管理器 则文本视图将返回到设计的 TextKit 1 在初始化期间创建 所有的 TextKit 2 对象 稍后将其丢弃 这是非常低效的 根据时间的不同 还有潜在的用户副作用 如果它发生在键入过程中 文本视图可能会失去焦点 并中断输入 需要再次选择文本视图才能恢复 可以通过在初始化时选择文本视图 来避免这种情况 既然您已经了解了所有的兼容性模式 现在是时候谈谈如何通过更新 App 和采用 TextKit 2 来完全避免它了 我想让您记住一件非常重要的事
每个文本视图只能有一个布局管理器 文本视图不能 同时拥有 NSTextLayoutManager 和 NSLayoutManager
一旦文本视图切换到 TextKit 1 就无法自动返回 切换布局系统的过程代价很高 并且您会丢失切换时 存在的任何 UI 状态 因此为了优化性能和可用性 系统不会将文本视图 从 TextKit 1 切换回 TextKit 2 这是单向操作
这意味着避免兼容模式非常重要 文本视图进入兼容模式 有几个不同的原因 文本视图进入兼容模式的第一个原因 是访问文本视图的 layoutManager 属性 其他原因则不太常见
因此一个重要的策略是 避免访问文本视图的布局管理器属性 还应避免通过文本视图的 文本容器访问布局管理器 检查代码中这些属性的使用情况 并将其删除或替换为 与 TextKit 2 等效的属性
如果您将 App 部署到 没有 TextKit 2 的旧操作系统版本 您可能无法 完全删除 layoutManager 代码
在这种情况下 应首先检查 文本视图的 NSTextLayoutManager
将 TextKit 2 代码放在 if 子句中 将 TextKit 1 代码放在 else 子句中 包括 layoutManager 访问权限 这样 TextKit 1 代码 仅在 TextKit 2 不可用时运行 并且 layoutManager 查询不会导致 意外回退到 TextKit 1
如果您遵循了所有这些建议 但仍然遇到了 来自系统的 对 TextKit 1 的意外回退 那就是我们的问题了 请向“反馈助手”报告此问题 在回退时包含堆栈跟踪的捕获 您可以通过在 UIKit 中的下划线 UITextViewEnableingCompatibilityMode 上 断开获取 也可以通过 AppKit 中的 willSwitchToNSLayoutManagerNotification 来获取
好的 现在我将从 NSLayoutManager 开始 详细介绍与 TextKit 1 类型 相关的更新代码 审核了 NSLayoutManager 查询的代码后 您需要找出与 NSTextLayoutManager 等价的 TextKit 2
一些布局管理器 API 在 TextKit 1 和 2 之间有相似的名称 替换很简单 这里有几个例子 在 TextKit 1 中 在 NSLayoutManager 上调用 usedRect (for: textContainer) 来获取文本容器内文本的边框 在 TextKit 2 中 您可以从 NSTextLayoutManager 上的 usageBoundsForTextContainer 属性中获得此信息
在 TextKit 1 中 我们使用临时属性 来表示只影响渲染 而不影响布局的属性 在 TextKit 2 中 我们更准确地将这些称为渲染属性
但是有一些 TextKit 1 API 在 TextKit 2 中没有直接的对应 API 要了解原因 您需要了解 印度文 (如卡纳达语) 脚本中 有些单词没有 正确的字符来进行字形映射 在这些脚本中 字形可以被分割 重新排序 重组 甚至删除
NSLayoutManager 上 基于字形的 API 假设您可以直接 将连续的字符范围 与连续的字形范围相关联 但这并不适用于所有脚本 使用这些 API 可能会导致 用卡纳达语等脚本编写的 文本的布局和呈现中断 这就是为什么 TextKit 2 中的 字形 API 为零的原因 您不能仅仅用单个 TextKit 2 API 替换 TextKit 1 glyph API 替换这些 API 需要不同的方法
这里是如何更新基于字形的代码 第一步是识别您正在使用的字形 API 接下来 看看您要如何使用这些 API 并从高级别上定义您想做什么 基于字形的代码是非常低级的 并且有许多细节与您的高级任务无关
定义了高级任务后 请检查 TextKit 2 中可用的结构 例如布局片段 行片段和文本选择 这些可以帮助您完成任务 例如 考虑下面的 TextKit 1 代码 这里使用了两个字形 API numberOfGlyphs 和 lineFragmentRect (for glyphat:index) 这段 TextKit 1 代码 历遍了文档中的所有字形 并计算行片段的长度 高级任务是计算文本视图中 已包装文本的行数 由于此代码使用的是行片段 rect 因此要使用的 TextKit 2 结构 是 NSTextLineFragment 和 NSTextLayoutFragment 这是为使用 TextKit 2 而重写的代码 它没有历遍字形 而是枚举文档中的文本布局片段 并提供一个闭包来计算 每个布局片段中的所有文本行片段
在为 TextKit 2 更新自己的代码时 请记住这个示例 现在我将换个方向 讨论如何更新基于 NSRange 的代码
TextKit 1 使用 NSRange 索引文本内容 NSRange 是字符串的线性索引 对于文本 Hello TextKit 2! 感叹号表示 代表“TextKit 2 exclamation point” 的 NSRange 的 位置是 6 长度是 10 因为它从第 6 个字符开始 长度是 10 个字符 这种线性模型很容易理解 对于字符串的索引非常有效
但是线性模型不适用于索引 任何比字符串更具结构的内容 举一个例子 HTML 文档被表示为一个树形结构 其中每个标签是树中的一个节点 如果我们的 Hello TextKit 2!文本 是 HTML 文档的一部分 我们的 NSRange 无法告诉我们 文本在 span 标签内 嵌套了 3 层 线性模型的表达能力 不足以存储该信息 所以我们不能用它来索引 像这样的嵌套结构 这就是为什么 TextKit 2 添加了 新类型来表示文本内容中的范围 NSTextLocation 是一个 能够表示文本内容中 单个位置的对象 NSTextRange 由开始和结束位置组成 结束位置不在该范围内 这些新类型可以通过 将位置定义为 DOM 节点 加上字符偏移量 来表示这个 HTML 文档的嵌套结构
既然 NSTextLocation 是一个协议 那任何自定义对象都可以是一个位置 只要它实现了 NSTextLocation 协议方法 这对于处理不同类型的支持模型中 结构化数据的后备存储来说 是至关重要的基础设施
但是文本视图是建立在 没有这种结构的 NSAttributedString 后台存储上的 我们无法在不破坏 许多 App 的情况下改变这一点 包括您的 App 因此 在使用 selectedRange 或 scrollRangeToVisible 等 文本视图 API 时 您将继续使用 NSRange 布局管理器或内容管理器 当与 TextKit 2 通信时 您需要在 NSRange 和 NSTextRange 之间进行转换
要将文本视图的 NSRange 转换为 NSTextRange 请将位置定义为 属性化字符串的整数索引
使用 NSRange 位置 作为 NSTextRange 的起始位置
使用 NSRange 位置加上长度 作为 NSTextRange 的结束位置 从概念上讲 这就是从 NSRange 映射到 NSTextRange 的方法
实际上 代码看起来有点不同 因为 NSTextLocations 必须是对象
您需要通过内容管理器来计算位置
对于开始位置 请向内容管理员询问 文档开始的位置 然后根据 NSRange 的位置 对其进行偏移 然后将起始位置偏移 NSRange 的长度以获得结束位置
要朝另一个方向发展 请使用文本内容管理器 以获取两个不同的偏移量
NSRange 的位置是文档开头 和 NSTextRange 位置之间的偏移量 NSRange 的长度是 NSTextRange 的 开始和结束位置 之间的偏移量
UITextViews 和 UITextFields 符合 UITextInput 协议 该协议使用 UITextPosition 和 range 大多数时候 在使用 UITextView 或 UITextField 时 您不需要将 UITextRange 直接转换为 NSTextRange 如果您想这样做 使用整数偏移量 作为两个范围类型之间的媒介
另一方面 如果您使用 带有 UITextInput 的自定义视图 您可以直接控制视图中 使用的 UITextPosition 和 UITextRange 子类 您可以使您的 UITextPosition 子类 符合 NSTextLocation 实现所需的方法 并使用您的子类直接创建 NSTextRanges
最后 这里提醒您避免 跨不同视图重用 UITextPosition 对象 即使两个视图中的内容相似 UITextPosition 仅对 用于创建它的视图有效
好了 现在您已经有了很多 让代码现代化的策略 应用这些策略 您的 App 就可以 享受到 TextKit 2 的好处
这就是 TextKit 和文本视图的 新增功能 我介绍了 TextKit 2 中的 许多重大改进 并分享了一些更新 App 的策略 同时保持与旧操作系统版本的兼容性 今天就在您的 App 中使用 TextKit 2 充分利用新的改进吧 检查您的文本视图 以确保 它们不会无意中退回到 TextKit 1 最后 采用现代化策略 让您的 App 在 TextKit 2 上运行 我们迫不及待地想阅读您使用 TextKit 2 和文本视图创建的内容了 感谢收看
-
-
13:21 - Check for NSTextLayoutManager first
if let textLayoutManager = textView.textLayoutManager { // TextKit 2 code goes here } else { let layoutManager = textView.layoutManager // TextKit 1 code goes here }
-
17:41 - Counting number of lines of wrapped text in a text view with TextKit 2
// Example: Updating glyph-based code var numberOfLines = 0 let textLayoutManager = textView.textLayoutManager textLayoutManager.enumerateTextLayoutFragments(from: textLayoutManager.documentRange.location, options: [.ensuresLayout]) { layoutFragment in numberOfLines += layoutFragment.textLineFragments.count }
-
21:10 - Convert NSRange to NSTextRange
let textContentManager = textLayoutManager.textContentManager let startLocation = textContentManager.location(textContentManager.documentRange.location, offsetBy: nsRange.location)! let endLocation = textContentManager.location(startLocation, offsetBy: nsRange.length) let nsTextRange = NSTextRange(location: startLocation, end: endLocation)
-
21:40 - Convert NSTextRange to NSRange
let textContentManager = textLayoutManager.textContentManager let location = textContentManager.offset(from: textContentManager.documentRange.location, to: nsTextRange!.location) let length = textContentManager.offset(from: nsTextRange!.location, to: nsTextRange!.endLocation) let nsRange = NSRange(location: location, length: length)
-
22:02 - Convert UITextRange to NSTextRange
let offset = textView.offset(from: textview.beginningOfDocument, to: uiTextRange.start) let startLocation = textContentManager.location(textContentManager.documentRange.location, offsetBy: offset)! let nsTextRange = NSTextRange(location: startLocation)
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。