SwiftUI:点击@FocusedField绑定的TextField外部如何关闭空输入框?
SwiftUI:实现点击空白区域关闭空输入框
问题场景
我是SwiftUI新手,正在做一个基于SwiftData的列表视图,功能是展示存储的项并支持添加新项。当前代码能实现:点击工具栏加号按钮后,列表底部弹出TextField并自动聚焦唤起键盘;按下回车键时,空输入直接关闭TextField,非空则创建新项后关闭。但点击TextField以外的区域时,输入框仍保持激活状态,希望实现:点击外部空白区域且输入为空时,自动关闭输入框,尝试过TapGesture但无效。
当前代码(含笔误)
import SwiftData import SwiftUI struct ItemList: View { @Environment(\.modelContext) private var modelContext @Query private var items: [Item] @State private var isAdding = false @State private var newItemName = "" // 这里定义的是newItemName @FocusState private var focusedField: String? var body: some View { NavigationStack { List { Section(header: Text("Items")) { ForEach(items) { item in NavigationLink { Text("Placeholder") } label: { Text(item.name) } .swipeActions(edge: .trailing) { Button(role: .destructive) { modelContext.delete(item) } label: { Label("Delete Item", systemImage: "trash.fill") } } } if isAdding { // 这里误用了newItemTitle,应该是newItemName TextField("Title", text: $newItemTitle, onCommit: { if !newItemTitle.isEmpty { let newItem = Item(name: newItemTitle) modelContext.insert(newItem) } isAdding = false newItemTitle = "" focusedField = nil }) .focused($focusedField, equals: "newItem") .onAppear { focusedField = "newItem" } .onDisappear { isAdding = false newItemTitle = "" focusedField = nil appearedCount = 0 // 未定义变量,需删除 } } } } .listStyle(.insetGrouped) .toolbar { ToolbarItem { Button { isAdding = true } label: { Label("New Item", systemImage: "plus") } } } } } } #Preview { ItemList() .modelContainer(for: Item.self, inMemory: true) }
问题分析
- 代码存在笔误:
newItemName和newItemTitle变量混用,导致逻辑失效 - 直接给视图加TapGesture会被List/NavigationStack的内置手势拦截,无法触发
- 未监听输入框的失焦事件,无法在用户点击外部时判断是否关闭输入框
解决方案(修正+功能实现)
步骤1:修正笔误
将所有newItemTitle替换为newItemName,删除未定义的appearedCount变量。
步骤2:监听失焦事件(核心逻辑)
利用@FocusState的变化监听输入框是否失焦,结合输入内容是否为空,自动关闭输入框。
最终完整代码
import SwiftData import SwiftUI struct ItemList: View { @Environment(\.modelContext) private var modelContext @Query private var items: [Item] @State private var isAdding = false @State private var newItemName = "" @FocusState private var focusedField: String? var body: some View { NavigationStack { List { Section(header: Text("Items")) { ForEach(items) { item in NavigationLink { Text("Placeholder") } label: { Text(item.name) } .swipeActions(edge: .trailing) { Button(role: .destructive) { modelContext.delete(item) } label: { Label("Delete Item", systemImage: "trash.fill") } } } if isAdding { TextField("Title", text: $newItemName, onCommit: { handleCommit() }) .focused($focusedField, equals: "newItem") .onAppear { focusedField = "newItem" } // 监听失焦事件 .onChange(of: focusedField) { oldValue, newValue in if oldValue == "newItem" && newValue == nil { if newItemName.isEmpty { isAdding = false newItemName = "" } } } } } } .listStyle(.insetGrouped) .toolbar { ToolbarItem { Button { isAdding = true } label: { Label("New Item", systemImage: "plus") } } } // 可选:给背景加高优先级手势,确保点击空白区域能触发 .background( Color.clear .onTapGesture { if focusedField == "newItem" && newItemName.isEmpty { focusedField = nil } } .highPriorityGesture(TapGesture()) ) } } // 抽离提交逻辑,代码更清晰 private func handleCommit() { guard !newItemName.isEmpty else { isAdding = false newItemName = "" focusedField = nil return } let newItem = Item(name: newItemName) modelContext.insert(newItem) isAdding = false newItemName = "" focusedField = nil } } #Preview { ItemList() .modelContainer(for: Item.self, inMemory: true) }
关键说明
onChange(of: focusedField):监听输入框焦点变化,当从激活状态变为失焦时,检查输入内容是否为空,为空则关闭输入框- 背景的
highPriorityGesture:确保点击NavigationStack的空白区域时,手势能优先触发,避免被List的手势拦截 - 抽离
handleCommit()方法:让代码结构更清晰,便于维护
内容的提问来源于stack exchange,提问作者soapergem
相关产品推荐
相关产品推荐

