Highlight
iOS 15 为 SwiftUI 引入了
@FocusState属性包装器和focused()修饰符,让开发者可以程序化控制焦点移动、验证表单时自动回退到错误字段、以及通过focusSection()在 tvOS 上构建大范围的焦点导航目标。
核心内容
做表单类 App 时,键盘和焦点管理一直是痛点。
以前的做法是混用 UIKit 的 becomeFirstResponder() 和 resignFirstResponder(),通过 UIViewRepresentable 桥接,代码繁琐且平台不一致。tvOS 上更痛苦——方向键的焦点跳转几乎不可控。
iOS 15 的 SwiftUI 用一套 API 解决了全平台的焦点管理问题。
程序化控制焦点
@FocusState 是一个新的属性包装器,专门用于追踪和管理焦点状态。
enum Field: Hashable {
case email
case password
}
struct LoginView: View {
@FocusState private var focusedField: Field?
@State private var email = ""
@State private var password = ""
var body: some View {
VStack {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
}
}
(03:49)
关键点:
focusedField是可选类型,nil 表示没有焦点在该表单区域Field枚举可以是任何Hashable类型,字符串、整数都可以- 修改
focusedField的值,焦点会自动移动到对应视图
表单验证与焦点回退
VStack {
TextField("Email", text: $email)
.focused($focusedField, equals: .email)
.border(Color.red, width: (focusedField == .email && !isEmailValid) ? 2 : 0)
SecureField("Password", text: $password)
.focused($focusedField, equals: .password)
}
.onSubmit {
submittedEmail = email
if !isEmailValid {
focusedField = .email
} else {
focusedField = nil // 验证通过,收起键盘
}
}
(06:07)
当用户点击键盘上的 Go 按钮,onSubmit 被触发。如果邮箱格式不对,焦点自动回到邮箱输入框,同时显示红色边框提示。验证通过后,设置 focusedField = nil,键盘自动收起。
tvOS 上的大范围焦点导航
tvOS 的焦点导航基于相邻关系。如果两个可聚焦视图不相邻,方向键无法跳转过去。
focusSection() 修饰符可以把一个容器视图标记为焦点区域,让焦点可以”跳跃”到非相邻的视图。
HStack {
VStack {
TextField("Email", text: $email)
SecureField("Password", text: $password)
}
.focusSection() // 左侧焦点区域
VStack {
Image(photoName)
Button("Browse Photos") {}
}
.focusSection() // 右侧焦点区域
}
(09:47)
加了 focusSection() 后,用户在左侧输入框按遥控器右键,焦点会直接跳到右侧的 Browse Photos 按钮,即使它们在屏幕上的距离很远。
详细内容
完整的登录表单示例
import SwiftUI
import AuthenticationServices
enum Field: Hashable {
case email
case password
}
struct ContentView: View {
@FocusState private var focusedField: Field?
@State private var email: String = ""
@State private var password: String = ""
@State private var submittedEmail: String = ""
var body: some View {
VStack(alignment: .center) {
Text("Vacation Planner")
.font(.custom("Baskerville-SemiBoldItalic", size: 60))
TextField("Email", text: $email)
.submitLabel(.next)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.padding()
.frame(height: 50)
.background(Color.white.opacity(0.9))
.cornerRadius(15)
.padding(10)
.focused($focusedField, equals: .email)
.border(Color.red,
width: (focusedField == .email && !isEmailValid) ? 2 : 0)
SecureField("Password", text: $password)
.submitLabel(.go)
.textContentType(.password)
.padding()
.frame(height: 50)
.background(Color.white.opacity(0.9))
.cornerRadius(15)
.padding(10)
.focused($focusedField, equals: .password)
SignInWithAppleButton(.signIn) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
switch result {
case .success:
print("Authorization successful.")
case .failure(let error):
print("Authorization failed: " + error.localizedDescription)
}
}
.frame(height: 50)
.cornerRadius(15)
}
.onSubmit {
submittedEmail = email
if !isEmailValid {
focusedField = .email
} else {
focusedField = nil
}
}
}
private var isEmailValid: Bool {
let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return submittedEmail.isEmpty || predicate.evaluate(with: submittedEmail)
}
}
(06:25)
关键点:
.submitLabel(.next)和.submitLabel(.go)控制键盘右下角的按钮文字.textContentType(.emailAddress)让系统提供自动填充建议.keyboardType(.emailAddress)弹出适合输入邮箱的键盘布局.border()的条件表达式根据焦点状态和验证结果动态显示红色边框
跨平台焦点管理
@FocusState 不限于文本输入框。任何可聚焦的视图都可以使用,包括 iOS、tvOS、watchOS 和 macOS。
struct SettingsView: View {
@FocusState private var focusedButton: String?
var body: some View {
VStack {
Button("Notifications") {}
.focused($focusedButton, equals: "notifications")
Button("Privacy") {}
.focused($focusedButton, equals: "privacy")
Button("About") {}
.focused($focusedButton, equals: "about")
}
}
}
核心启发
1. 为复杂表单添加智能导航
在注册流程中,用户填完一个字段按回车后,自动将焦点移到下一个必填项。如果当前字段验证失败,焦点留在原地并显示错误提示。入口 API:@FocusState + .submitLabel() + .onSubmit()。
2. 实现一键清空搜索框
在搜索界面添加一个清除按钮,点击后清空搜索文本并自动重新聚焦到搜索框,用户可以直接输入新关键词。入口 API:focusedField = .search。
3. 为 tvOS 应用设计侧边栏+内容布局
用 focusSection() 把侧边栏和内容区标记为两个独立的焦点区域,用户用遥控器方向键可以在区域间自由跳转,无需逐个遍历中间的所有元素。入口 API:.focusSection()。
4. 构建多步骤向导式界面
在设置向导中,每个步骤完成后自动将焦点移到下一步的第一个输入框。用 @FocusState 枚举表示步骤和字段的组合状态。入口 API:@FocusState + withAnimation。
5. 为 macOS 应用添加快捷键导航
结合 keyboardShortcut() 和 @FocusState,让用户按 Tab 键在表单字段间循环,按 Cmd+Enter 提交。入口 API:.keyboardShortcut(.tab) + @FocusState。
关联 Session
- Add rich graphics to your SwiftUI app — 图形绘制与焦点视觉反馈
- Demystify SwiftUI — 理解视图 Identity 与焦点状态的关系
- SwiftUI on the Mac: Build the fundamentals — macOS 上的焦点管理实践
- Discover concurrency in SwiftUI — 异步操作后的焦点恢复
评论
GitHub Issues · utterances