若要插入建议而不应用文本框的值,请将补全与提交分开处理。在 macOS 15 或更高版本中,SwiftUI 提供原生文本建议;使用 submitScope() 阻止祖先视图的提交处理程序,并通过显式按钮执行最终操作。
本教程将在你自己的 Mac 应用中构建一个小型标签选择器。输入 Bl 时,会显示诸如 Blue Tile 这样的合成标签。选择建议会更新可编辑草稿;点击 应用标签 会将草稿复制到已应用值。这个区别很重要:阻止应用操作与截获 Return 键命令是两个不同的需求。
选择符合交互需求的 API
Apple 的 textInputSuggestions(_:) 修饰符可为文本框显示选项。每个可操作的建议都需要 textInputCompletion(_:),用来提供替换字符串。这两个 API 的文档均注明适用于 macOS 15 及更高版本。没有补全修饰符的建议可以显示,但无法响应点击。Apple:textInputSuggestions(_:)、Apple:textInputCompletion(_:)
对于简短的标签文本框,替换整个草稿是合适的。Apple 的建议文档介绍了这种整字段替换方式。不要假设此 API 提供相对于插入点的词条编辑器或行内后缀预览;这些功能需要更具体的编辑设计。Apple:textInputSuggestions(_:)
| 你的需求 | 合适的起点 |
|---|---|
| 在 macOS 15 或更高版本中提供完整标签 | SwiftUI 原生文本建议 |
| 阻止文本框调用祖先视图的提交处理程序 | 在文本框上使用 submitScope() |
| 使用自定义建议布局或支持更早的部署目标 | 由应用管理、可更新草稿绑定的建议按钮 |
| 让 Return 接受高亮项并截获该命令 | 由应用管理且带有命令处理功能的 AppKit 控件 |
| 预览后缀或替换插入点处的单词 | AppKit 文本补全和显式补全范围 |
最后两行是根据文档中的命令和补全 API 得出的架构选择。当交互需要比替换字符串和提交边界更精细的控制时,这些选择会很有用。
构建完整的 SwiftUI 标签选择器
在 Xcode 中创建一个 macOS App 项目,并使用包含 macOS 15 API 的 SwiftUI SDK。将部署目标设为 macOS 15 或更高版本。将生成的应用和内容视图声明替换为以下单个源文件,并确保目标中只保留一个 @main 应用声明。
示例将 draft 和 appliedLabel 分开保存。示例中的本地匹配规则刻意保持简单:区分大小写的前缀匹配、文本为空时不提供建议、精确匹配时不显示建议。Swift 文档将 hasPrefix(_:) 定义为字符串前缀比较;更宽泛的匹配规则应由应用自行决定。Swift:Strings and Characters
import SwiftUI
@main
struct LabelCompletionApp: App {
var body: some Scene {
WindowGroup {
LabelCompletionView()
}
}
}
struct LabelCompletionView: View {
private let labels = [
"Blue Tile",
"Blue Tray",
"Green Tile",
"Round Tray"
]
@State private var draft = ""
@State private var appliedLabel = ""
@State private var applyCount = 0
@FocusState private var fieldIsFocused: Bool
private var matches: [String] {
guard !draft.isEmpty else { return [] }
return labels.filter {
$0.hasPrefix(draft) && $0 != draft
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a label")
.font(.headline)
TextField("Type Bl to see suggestions", text: $draft)
.textFieldStyle(.roundedBorder)
.focused($fieldIsFocused)
.textInputSuggestions {
ForEach(matches, id: \.self) { label in
Text(label)
.textInputCompletion(label)
}
}
.submitScope()
Button("Apply Label") {
appliedLabel = draft
applyCount += 1
}
.disabled(draft.isEmpty)
Text("Draft: \(draft)")
Text("Applied: \(appliedLabel)")
Text("Apply count: \(applyCount)")
}
.padding(24)
.frame(width: 420)
.onAppear {
fieldIsFocused = true
}
}
}
这是一个完整的应用示例,包含示例标签和状态。我们已根据检索到的 API 文档检查了代码,但此处未编译或运行,因为环境中没有 Swift 工具链和 macOS UI 运行环境。
为什么选择建议不会应用标签
每个补全项都会向文本框提供一个字符串,而文本框绑定到 draft。只有 应用标签 按钮的操作会赋值给 appliedLabel 或递增 applyCount。因此,在此示例中,更新草稿不会触发最终操作。
文本框还使用了 submitScope()。Apple 的文档说明,此修饰符可阻止提交触发传递到视图层级中更高位置的提交修饰符。当该组件嵌入带有祖先 onSubmit 的大型表单时,这一点很重要。Apple:submitScope(_:)
初始焦点通过 @FocusState 设置。Apple 的文档说明,以编程方式为其绑定值赋值会将焦点移至关联视图。此处的焦点设置是为了方便编辑,不会控制是否应用标签。Apple:FocusState
示例有意允许的操作
标签列表提供的是建议,并不限制有效输入。输入 Square Tile 仍会启用 应用标签。如果界面只允许使用列表中的标签,请明确修改校验规则,例如仅当草稿与目录条目相符时才启用按钮。不要静默地将无法匹配的草稿替换为第一条建议。
示例还会替换整个标签。如果草稿包含多个单词,选择补全项应视为选择最终的字段值。需要保留周围文本的词条编辑器则需要依据选区进行替换。
理解“不提交”的含义
SwiftUI 的 onSubmit(of:_:) 文档说明,用户按下 Return 时,文本框会触发其提交操作。提交操作可以附加到单个视图,也可以附加到更大的视图层级。如果自动补全选择似乎触发了意外的应用操作,请检查这两处设置。Apple:onSubmit(of:_:)
submitScope() 为祖先处理程序设定边界。它的文档并未保证不会发生内部提交事件、编辑会持续进行,或所有键盘命令都会被截获。同样,Apple 对补全修饰符的说明是文本替换,并未承诺一律抑制 Return 键行为。选择交互方式时请牢记这些限制。Apple:submitScope(_:)、Apple:textInputCompletion(_:)
对于现有组件,请分别检查以下路径:
- 如果 Return 绝不能应用字段值,请从文本框自身的
onSubmit中移除最终应用操作。 - 在文本框上添加
submitScope(),以阻止祖先提交操作。 - 单独检查按钮键盘快捷键,尤其是显式默认操作。
- 检查任何变更观察器或模型设置器,确认它们不会在草稿变化时执行最终操作。
空的本地 onSubmit 闭包并非文档所述的提交边界替代方案。Apple 特别提供了 submitScope() 来阻止事件传播。如果应用只应在没有活动补全项时响应 Return,则需要额外状态和命令处理来实现这种条件键盘策略。Apple:onSubmit(of:_:)
需要 Return 确认并截获时使用 AppKit
对于由应用管理的 NSTextField,AppKit 提供了 control(_:textView:doCommandBy:)。其文本视图参数是该控件的字段编辑器。代理可以处理绑定命令并返回 true;返回 false 则表示该代理未处理该命令。这是文档规定的自定义文本框命令扩展点。Apple:control(_:textView:doCommandBy:)
自定义补全控制器可以通过该扩展点按以下顺序处理:
- 跟踪建议列表是否打开,以及哪个项目处于高亮状态。
- 收到 Return 命令且存在有效的高亮项目时,将该项目插入草稿。
- 关闭建议列表并同步应用的草稿模型。
- 返回
true,因为控制器已处理该命令。 - 对控制器未处理的命令返回
false。
这套顺序是根据代理约定推导出的实现设计,并非内置的自动补全策略。还要单独决定列表关闭时 Return 应执行什么操作。如果你管理的是独立的 NSTextView,相应的钩子是 textView(_:doCommandBy:);Apple 指出,返回 true 会阻止文本视图自行执行该命令。Apple:textView(_:doCommandBy:)
对于文本补全本身,NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) 支持替换和选区变更。使用 isFinal: false 时,会选中建议的后缀;使用 isFinal: true 时,会在补全单词后折叠选区。这个标记描述的是补全状态,而非应用提交状态。movement 参数用于标识补全导航或选择的方式。Apple:insertCompletion(_:forPartialWordRange:movement:isFinal:)
保留正确的文本范围
AppKit 的 rangeForUserCompletion 用于标识从部分单词开头到插入点的范围。请使用符合预期替换内容的范围,并在编辑或插入点移动后重新计算。文本变更后不要盲目复用之前捕获的范围。Apple:rangeForUserCompletion
转换文本范围时,不要用 Swift 的字符数代替 NSString 长度。Swift 文档解释了 String 统计扩展字素簇,而 NSString 长度统计 UTF-16 代码单元。这些值可能不同。SwiftUI 示例采用整字段绑定替换,因此完全避免了这种范围转换。Swift:Strings and Characters)
分别验证插入和应用
请通过可见的草稿、已应用值和计数器,检查每个受支持 macOS 版本上的结果。以下是针对该示例的预期检查项,并非已报告的测试结果。
| 操作 | 预期的可观察结果 |
|---|---|
输入 Bl |
草稿为 Bl;显示匹配标签建议 |
点击 Blue Tile |
草稿变为 Blue Tile;已应用值和计数保持不变 |
| 编辑时按 Return | 应用不会应用草稿 |
点击 应用标签 |
已应用值复制草稿;计数增加一次 |
| 之后编辑草稿 | 之前已应用的值仍然可见 |
| 清空草稿 | 建议列表为空,且 应用标签 已禁用 |
输入 bl |
根据示例的区分大小写规则,没有匹配项 |
还要在原生建议列表中通过键盘选择,并将文本框嵌入其实际父视图后重复这些检查。Apple 指出,移动 macOS 窗口等事件可能会关闭原生建议界面,因此即使没有该列表,草稿仍应可用。Apple:textInputSuggestions(_:)
如果部署目标早于 macOS 15,请提供经过可用性检查的原生视图和回退方案,或始终使用由应用管理的建议按钮。回退按钮可以直接将标签赋值给草稿,并将最终操作留给 应用标签。这样可以实现同样的草稿与应用操作分离,而键盘导航和显示逻辑仍由你的组件负责。
对于这个标签选择器,界限可以通过可观察结果来确认:接受补全会更改可编辑草稿,只有显式的最终操作会更改已应用标签。使用 SwiftUI 原生建议和提交范围来实现这个边界;如果需求还包括截获 Return 本身,则改用 AppKit 命令处理。
来源
- textInputSuggestions(_:) — Apple
- textInputCompletion(_:) — Apple
- submitScope(_:) — Apple
- onSubmit(of:_:) — Apple
- FocusState — Apple
- control(_:textView:doCommandBy:) — Apple
- textView(_:doCommandBy:) — Apple
- insertCompletion(_:forPartialWordRange:movement:isFinal:) — Apple
- rangeForUserCompletion — Apple
- Strings and Characters — Swift.org