SwiftUI 焦点系统 & 键盘内部

发布: (2025年12月28日 GMT+8 05:36)
7 min read
原文: Dev.to

Source: Dev.to

Sebastien Lato

SwiftUI 的焦点看起来很简单:

@FocusState var isFocused: Bool

但事实并非如此。

这时你会遇到诸如:

  • 焦点随机丢失
  • 键盘意外收起
  • 焦点在字段之间跳动
  • 表单导航失效
  • ScrollView 与键盘相互冲突
  • 可访问性焦点表现异常
  • 导航后焦点未恢复

本文解释了 SwiftUI 焦点在内部是如何工作的,它如何与键盘、导航、滚动视图以及可访问性交互——以及如何在生产应用中正确使用它。

🧠 心智模型:焦点是状态 + 路由

SwiftUI 的焦点 不仅仅 是一个布尔值。从内部来看,它是:

  • 一个焦点图
  • 由状态驱动
  • 通过视图层次结构解析
  • 与键盘 + 可访问性协同

把焦点视作输入的导航。

🧩 1. Focus 是基于值的(而非基于视图的)

最重要的规则。

错误的思维模型: “这个 TextField 拥有焦点。”
正确的思维模型: “焦点是指向某个字段的状态。”

这就是它能工作的原因:

enum Field {
    case email
    case password
}

@FocusState private var focusedField: Field?

SwiftUI 通过以下方式解析焦点:

  1. 匹配聚焦的值
  2. 遍历视图树
  3. 找到第一个兼容的焦点目标

🧱 2. SwiftUI 如何构建焦点树

在渲染时,SwiftUI:

  • 扫描可聚焦视图
  • 构建焦点树
  • 为每个视图分配焦点标识
  • 解析活动焦点状态

可聚焦视图包括:

  • TextField
  • SecureField
  • TextEditor
  • 自定义可聚焦控件
  • 可访问性元素

如果视图消失 → 其焦点节点将被移除。

🔄 3. 为什么焦点会“随机”丢失

焦点会在以下情况下丢失:

  • 焦点视图离开层级
  • 视图的标识发生变化
  • 焦点状态值改变
  • 导航移除该视图
  • 父视图被重新创建
  • ScrollView 重新布局内容
  • 触发键盘收起

这并非随机——而是 标识 + 生命周期

🧠 4. Focus vs. View Identity (Critical Connection)

这会打破焦点:

TextField("Email", text: $email)
    .id(UUID()) // ❌

为什么?

  • 标识改变
  • 焦点节点被销毁
  • 焦点状态指向空
  • 键盘被收起

规则: 📌 焦点需要 稳定的视图标识

⌨️ 5. 键盘是副作用,而非根源

SwiftUI focus 控制键盘——而不是相反。

流程:

Focus change

Responder change

Keyboard presentation

这就是为什么在 不更新 focus 的情况下手动关闭键盘会导致 bug。

正确的关闭方式:

focusedField = nil

避免:

  • 强制 resignFirstResponder
  • UIKit hack
  • 在未更新 focus 的情况下使用手势关闭

📜 6. ScrollView + Keyboard Internals

当键盘出现时,SwiftUI 会:

  • 调整安全区 inset
  • 尝试保持焦点字段可见
  • 可能会自动滚动
  • 如果布局过于复杂,可能会失败

常见问题:

  • 嵌套的 ScrollView
  • GeometryReader 的使用
  • 自定义布局
  • 动态高度变化

最佳实践:

  • 保持表单简洁
  • 在表单中避免使用 GeometryReader
  • 适当使用 .scrollDismissesKeyboard(.interactively)

🧭 7. 编程式聚焦(正确方式)

正确的写法:

focusedField = .email

延迟聚焦(导航 / 动画)时:

Task {
    try await Task.sleep(for: .milliseconds(100))
    focusedField = .email
}

为什么要延迟?

  • 必须存在聚焦树
  • 必须渲染视图
  • 必须完成导航

🧪 8. 焦点与导航

在导航时:

  • 焦点 NOT 自动转移
  • 新视图默认未聚焦
  • 先前的焦点会被销毁

如果需要恢复焦点:

  • 将焦点状态存储在外部
  • onAppear 时恢复它
.onAppear {
    focusedField = savedFocus
}

♿ 9. 焦点 vs. 可访问性焦点

这些是不同的系统。

  • 输入焦点 → 键盘
  • 可访问性焦点 → VoiceOver

SwiftUI 协调它们,但:

  • 它们可能会分离
  • 可访问性可以独立移动焦点
  • 可访问性焦点并不总是触发键盘

永远不要假设它们是相同的。

🧠 10. 自定义可聚焦视图

您可以使自定义控件可聚焦:

.focusable()
.focused($focusedField, equals: .custom)

适用于以下情况:

  • 自定义输入
  • 类游戏的 UI
  • tvOS / visionOS
  • 高级键盘导航

⚠️ 11. 最大的焦点反模式

避免:

  • 内联 UUID 标识
  • 重新创建表单行
  • 混用 UIKit 响应者
  • 在未更新焦点的情况下关闭键盘
  • 将焦点逻辑放在视图中而不是 ViewModel 中
  • 在焦点切换期间进行大量布局更改

这些导致约 90 % 的焦点错误。

🧠 焦点系统速查表

  • ✔ 焦点是一种状态
  • ✔ 标识必须保持稳定
  • ✔ 键盘跟随焦点
  • ✔ 导航会破坏焦点
  • ✔ 延迟焦点直至视图存在
  • ✔ 可访问性焦点是独立的
  • ✔ 表单需要布局稳定性

🚀 最后思考

SwiftUI 的焦点并不脆弱——它非常精准。一旦你理解了:

  • 将焦点视为状态
  • 焦点树解析
  • 与键盘、导航和可访问性的关系

就可以构建可靠、可投入生产的表单,而无需常见的头痛问题。

身份 + 生命周期

  • 键盘作为副作用

Forms, editors, and input‑heavy screens become predictable and rock solid.

Back to Blog

相关文章

阅读更多 »

SwiftUI 手势系统内部

markdown !Sebastien Latohttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

SwiftUI 可访问性内部

可访问性是一棵平行视图树 SwiftUI 构建了两棵树: - 可视视图树 - 可访问性树 它们相关——但并不相同。 单个…

ScrollView 与 SwiftUI 中的坐标空间

基于滚动的 UI 在现代应用中随处可见——collapsing headers、parallax effects、sticky toolbars、section pinning、scroll‑driven animations、pull‑to‑refresh……