SwiftUI:如何让 TextField 成为第一响应者?
SwiftUI: How to make TextField become first responder?
这是我的 SwiftUI
代码:
struct ContentView : View {
@State var showingTextField = false
@State var text = ""
var body: some View {
return VStack {
if showingTextField {
TextField($text)
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
我想要的是当文本字段变得可见时,使文本字段成为第一响应者(即接收焦点并弹出键盘)。
Swift UI 3
从 Xcode 13 开始,您可以使用 focused
修饰符使视图成为第一响应者。
Swift UI 1/2
目前似乎不可能,但您可以自己实现类似的东西。
您可以创建自定义文本字段并添加一个值以使其成为第一响应者。
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
var didBecomeFirstResponder = false
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
@Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
注意:需要 didBecomeFirstResponder
来确保文本字段仅成为第一响应者一次,而不是在 SwiftUI
每次刷新时!
你会像这样使用它...
struct ContentView : View {
@State var text: String = ""
var body: some View {
CustomTextField(text: $text, isFirstResponder: true)
.frame(width: 300, height: 50)
.background(Color.red)
}
}
P.S。我添加了一个 frame
,因为它的行为不像股票 TextField
,这意味着幕后还有更多事情要做。
更多关于 Coordinators
的精彩 WWDC 19 演讲:
Integrating SwiftUI
测试于 Xcode 11.4
对于最终来到这里但使用@Matteo Pacini 的回答遇到崩溃的任何人,请注意 beta 4 中的此更改: 关于此块:
init(text: Binding<String>) {
$text = text
}
应该使用:
init(text: Binding<String>) {
_text = text
}
并且如果您想让文本字段成为 sheet
中的第一响应者,请注意在显示文本字段之前您不能调用 becomeFirstResponder
。换句话说,将@Matteo Pacini 的文本字段直接放在 sheet
内容中会导致崩溃。
要解决此问题,请额外检查 uiView.window != nil
文本字段的可见性。只有在视图层次结构中之后才关注:
struct AutoFocusTextField: UIViewRepresentable {
@Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context:
UIViewRepresentableContext<AutoFocusTextField>) {
uiView.text = text
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: AutoFocusTextField
init(_ autoFocusTextField: AutoFocusTextField) {
self.parent = autoFocusTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
iOS 15
有一个名为 @FocusState
的新包装器,用于控制键盘和聚焦键盘的状态('aka' firstResponder)。
成为第一响应者(专注)
如果您在文本字段上使用 focused
修饰符,您可以使它们成为焦点:
辞去第一响应者(关闭键盘)
或通过将变量设置为 nil
:
来关闭键盘
iOS 13 岁及以上:老了但还能工作!
简单的包装器结构 - 像本地人一样工作:
请注意 根据评论中的要求添加了文本绑定支持
struct LegacyTextField: UIViewRepresentable {
@Binding public var isFirstResponder: Bool
@Binding public var text: String
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, configuration: @escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
configuration(uiView)
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
@objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
用法:
struct ContentView: View {
@State var text = ""
@State var isFirstResponder = false
var body: some View {
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder)
}
}
奖励:完全可定制
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
[=12=].textColor = .red
[=12=].tintColor = .blue
}
此方法完全适用。例如,您可以看到 with the same method
使用SwiftUI-Introspect,您可以:
TextField("", text: $value)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
所选答案导致 AppKit 出现无限循环问题。我不知道 UIKit 案例。
为避免该问题,我建议直接共享 NSTextField
实例。
import AppKit
import SwiftUI
struct Sample1: NSViewRepresentable {
var textField: NSTextField
func makeNSView(context:NSViewRepresentableContext<Sample1>) -> NSView { textField }
func updateNSView(_ x:NSView, context:NSViewRepresentableContext<Sample1>) {}
}
你可以像这样使用它。
let win = NSWindow()
let txt = NSTextField()
win.setIsVisible(true)
win.setContentSize(NSSize(width: 256, height: 256))
win.center()
win.contentView = NSHostingView(rootView: Sample1(textField: txt))
win.makeFirstResponder(txt)
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.run()
这打破了纯值语义,但依赖于 AppKit 意味着您部分放弃了纯值语义并会带来一些不便。这是我们现在需要的一个魔法洞,用于处理 SwiftUI 中缺乏第一响应者控制的问题。
当我们直接访问 NSTextField
时,设置第一响应者是普通的 AppKit 方式,因此没有可见的麻烦源。
您可以下载工作源代码 here。
正如其他人所指出的(例如 , e.g. ), I have also not found any of the accepted solutions to work on macOS. I've had some luck, however, using NSViewControllerRepresentable to get a NSSearchField 在 SwiftUI 视图中显示为第一响应者:
import Cocoa
import SwiftUI
class FirstResponderNSSearchFieldController: NSViewController {
@Binding var text: String
init(text: Binding<String>) {
self._text = text
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let searchField = NSSearchField()
searchField.delegate = self
self.view = searchField
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
}
}
extension FirstResponderNSSearchFieldController: NSSearchFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.text = textField.stringValue
}
}
}
struct FirstResponderNSSearchFieldRepresentable: NSViewControllerRepresentable {
@Binding var text: String
func makeNSViewController(
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) -> FirstResponderNSSearchFieldController {
return FirstResponderNSSearchFieldController(text: $text)
}
func updateNSViewController(
_ nsViewController: FirstResponderNSSearchFieldController,
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) {
}
}
示例 SwiftUI 视图:
struct ContentView: View {
@State private var text: String = ""
var body: some View {
FirstResponderNSSearchFieldRepresentable(text: $text)
}
}
由于无法通过 SwiftUI 使用 Responder Chain,因此我们必须使用 UIViewRepresentable 使用它。
请看下面的 link,因为我已经做了一个解决方法,它的工作方式与我们过去使用 UIKit 的方式类似。
这是我的实现变体,基于@Mojtaba Hosseini 和@Matteo Pacini 解决方案。
我对 SwiftUI 还是个新手,所以我不能保证代码的绝对正确性,但它确实有效。
希望对大家有所帮助。
ResponderView:这是一个通用的响应者视图,可以与任何 UIKit 视图一起使用。
struct ResponderView<View: UIView>: UIViewRepresentable {
@Binding var isFirstResponder: Bool
var configuration = { (view: View) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> View { View() }
func makeCoordinator() -> Coordinator {
Coordinator($isFirstResponder)
}
func updateUIView(_ uiView: View, context: UIViewRepresentableContext<Self>) {
context.coordinator.view = uiView
_ = isFirstResponder ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
configuration(uiView)
}
}
// MARK: - Coordinator
extension ResponderView {
final class Coordinator {
@Binding private var isFirstResponder: Bool
private var anyCancellable: AnyCancellable?
fileprivate weak var view: UIView?
init(_ isFirstResponder: Binding<Bool>) {
_isFirstResponder = isFirstResponder
self.anyCancellable = Publishers.keyboardHeight.sink(receiveValue: { [weak self] keyboardHeight in
guard let view = self?.view else { return }
DispatchQueue.main.async { self?.isFirstResponder = view.isFirstResponder }
})
}
}
}
// MARK: - keyboardHeight
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { ([=10=].userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
struct ResponderView_Previews: PreviewProvider {
static var previews: some View {
ResponderView<UITextField>.init(isFirstResponder: .constant(false)) {
[=10=].placeholder = "Placeholder"
}.previewLayout(.fixed(width: 300, height: 40))
}
}
ResponderTextField - 这是一个方便的 ResponderView 文本字段包装器。
struct ResponderTextField: View {
var placeholder: String
@Binding var text: String
@Binding var isFirstResponder: Bool
private var textFieldDelegate: TextFieldDelegate
init(_ placeholder: String, text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.placeholder = placeholder
self._text = text
self._isFirstResponder = isFirstResponder
self.textFieldDelegate = .init(text: text)
}
var body: some View {
ResponderView<UITextField>(isFirstResponder: $isFirstResponder) {
[=11=].text = self.text
[=11=].placeholder = self.placeholder
[=11=].delegate = self.textFieldDelegate
}
}
}
// MARK: - TextFieldDelegate
private extension ResponderTextField {
final class TextFieldDelegate: NSObject, UITextFieldDelegate {
@Binding private(set) var text: String
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
}
struct ResponderTextField_Previews: PreviewProvider {
static var previews: some View {
ResponderTextField("Placeholder",
text: .constant(""),
isFirstResponder: .constant(false))
.previewLayout(.fixed(width: 300, height: 40))
}
}
以及使用方法。
struct SomeView: View {
@State private var login: String = ""
@State private var password: String = ""
@State private var isLoginFocused = false
@State private var isPasswordFocused = false
var body: some View {
VStack {
ResponderTextField("Login", text: $login, isFirstResponder: $isLoginFocused)
ResponderTextField("Password", text: $password, isFirstResponder: $isPasswordFocused)
}
}
}
这是一个适用于 introspect 的 ViewModifier。它适用于 AppKit MacOS,Xcode 11.5
struct SetFirstResponderTextField: ViewModifier {
@State var isFirstResponderSet = false
func body(content: Content) -> some View {
content
.introspectTextField { textField in
if self.isFirstResponderSet == false {
textField.becomeFirstResponder()
self.isFirstResponderSet = true
}
}
}
}
由于 SwiftUI 2 不支持第一响应者,但我使用此解决方案。它很脏,但可能适用于只有 1 UITextField
和 1 UIWindow
.
的某些用例
import SwiftUI
struct MyView: View {
@Binding var text: String
var body: some View {
TextField("Hello world", text: $text)
.onAppear {
UIApplication.shared.windows.first?.rootViewController?.view.textField?.becomeFirstResponder()
}
}
}
private extension UIView {
var textField: UITextField? {
subviews.compactMap { [=10=] as? UITextField }.first ??
subviews.compactMap { [=10=].textField }.first
}
}
要填补这个缺失的功能,您可以使用 Swift 包管理器安装 SwiftUIX:
- 在 Xcode 中,打开您的项目并导航至文件 → Swift 包 → 添加包依赖项...
- 粘贴存储库 URL (https://github.com/SwiftUIX/SwiftUIX) 并单击“下一步”。
- 对于规则,select 分支(分支设置为主)。
- 单击“完成”。
- 打开项目设置,将SwiftUI.framework添加到链接框架和库,将状态设置为可选。
更多信息:https://github.com/SwiftUIX/SwiftUIX
import SwiftUI
import SwiftUIX
struct ContentView : View {
@State var showingTextField = false
@State var text = ""
var body: some View {
return VStack {
if showingTextField {
CocoaTextField("Placeholder text", text: $text)
.isFirstResponder(true)
.frame(width: 300, height: 48, alignment: .center)
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
我们有一个解决方案,可以毫不费力地控制第一响应者。
https://github.com/mobilinked/MbSwiftUIFirstResponder
TextField("Name", text: $name)
.firstResponder(id: FirstResponders.name, firstResponder: $firstResponder, resignableUserOperations: .all)
TextEditor(text: $notes)
.firstResponder(id: FirstResponders.notes, firstResponder: $firstResponder, resignableUserOperations: .all)
响应链
我在 iOS 13+
上为 跨平台第一响应者处理制作了这个小包,没有子类化视图或在 SwiftUI 中制作自定义 ViewRepresentables
https://github.com/Amzd/ResponderChain
如何将其应用于您的问题
SceneDelegate.swift
...
// Set the ResponderChain as environmentObject
let rootView = ContentView().environmentObject(ResponderChain(forWindow: window))
...
ContentView.swift
struct ContentView: View {
@EnvironmentObject var chain: ResponderChain
@State var showingTextField = false
@State var text = ""
var body: some View {
return VStack {
if showingTextField {
TextField($text).responderTag("field1").onAppear {
DispatchQueue.main.async {
chain.firstResponder = "field1"
}
}
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
不是真正的答案,只是在 Casper 的伟大解决方案的基础上使用方便的修饰符构建 -
struct StartInput: ViewModifier {
@EnvironmentObject var chain: ResponderChain
private let tag: String
init(tag: String) {
self.tag = tag
}
func body(content: Content) -> some View {
content.responderTag(tag).onAppear() {
DispatchQueue.main.async {
chain.firstResponder = tag
}
}
}
}
extension TextField {
func startInput(_ tag: String = "field") -> ModifiedContent<TextField<Label>, StartInput> {
self.modifier(StartInput(tag: tag))
}
}
就像-
一样使用
TextField("Enter value:", text: $quantity)
.startInput()
iOS 15.0+
macOS 12.0+,
Mac 催化剂 15.0+,
tvOS 15.0+,
watchOS 8.0+
如果您只有一个 TextField,请使用 focused(_:)
。
focused(_:)
Modifies this view by binding its focus state to the given Boolean state value.
struct NameForm: View {
@FocusState private var isFocused: Bool
@State private var name = ""
var body: some View {
TextField("Name", text: $name)
.focused($isFocused)
Button("Submit") {
if name.isEmpty {
isFocued = true
}
}
}
}
如果您有多个 TextField,请使用 focused(_:equals:)
。
focused(_:equals:)
Modifies this view by binding its focus state to the given state value.
struct LoginForm: View {
enum Field: Hashable {
case usernameField
case passwordField
}
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
handleLogin(username, password)
}
}
}
}
}
SwiftUI 文档
更新
我在 xCode version 13.0 beta 5 (13A5212g)
中对此进行了测试。有效
在我的例子中,我想立即聚焦一个文本字段,我使用了 .onappear
函数
struct MyView: View {
@FocusState private var isTitleTextFieldFocused: Bool
@State private var title = ""
var body: some View {
VStack {
TextField("Title", text: $title)
.focused($isTitleTextFieldFocused)
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isTitleTextFieldFocused = true
}
}
}
}
扩展@JoshuaKifer 上面的回答,如果您在使用 Introspect 使文本字段成为第一响应者时处理导航动画出现故障。使用这个:
import SchafKit
@State var field: UITextField?
TextField("", text: $value)
.introspectTextField { textField in
field = textField
}
.onDidAppear {
field?.becomeFirstResponder()
}
有关此解决方案的更多详细信息 here。
我采用了一些不同的方法 - 我基于 UIView
制作了它,而不是基于 UITextField
的 UIViewRepresentable
并使用 [=16= 将其插入 SwiftUI 视图层次结构]修饰符。在 UIView
中,我添加了逻辑以在子视图和父视图中找到 canBecomeFirstResponder
的第一个视图。
private struct FocusableUIView: UIViewRepresentable {
var isFirstResponder: Bool = false
class Coordinator: NSObject {
var didBecomeFirstResponder = false
}
func makeUIView(context: UIViewRepresentableContext<FocusableUIView>) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}
func makeCoordinator() -> FocusableUIView.Coordinator {
return Coordinator()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<FocusableUIView>) {
guard uiView.window != nil, isFirstResponder, !context.coordinator.didBecomeFirstResponder else {
return
}
var foundRepsonder: UIView?
var currentSuperview: UIView? = uiView
repeat {
foundRepsonder = currentSuperview?.subviewFirstPossibleResponder
currentSuperview = currentSuperview?.superview
} while foundRepsonder == nil && currentSuperview != nil
guard let responder = foundRepsonder else {
return
}
DispatchQueue.main.async {
responder.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
private extension UIView {
var subviewFirstPossibleResponder: UIView? {
guard !canBecomeFirstResponder else { return self }
for subview in subviews {
if let firstResponder = subview.subviewFirstPossibleResponder {
return firstResponder
}
}
return nil
}
}
这是一个如何使用它使 TextField 自动对焦的示例(+ 奖金利用 @FocusState
新 iOS 15 api)。
extension View {
@ViewBuilder
func autofocus() -> some View {
if #available(iOS 15, *) {
modifier(AutofocusedViewModifiers.Modern())
} else {
modifier(AutofocusedViewModifiers.Legacy())
}
}
}
private enum AutofocusedViewModifiers {
struct Legacy: ViewModifier {
func body(content: Content) -> some View {
content
.background(FocusableUIView(isFirstResponder: isFocused))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
@State private var isFocused = false
}
@available(iOS 15, *)
struct Modern: ViewModifier {
func body(content: Content) -> some View {
content
.focused($isFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
@FocusState private var isFocused: Bool
}
}
内容视图示例:
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
TextField("placeholder", text: $text)
Text("some text")
}
.autofocus()
}
}
正确的 SwiftUI 方法是如上所述使用 @FocusState
。但是,此 API 仅适用于 iOS 15。如果您使用的是 iOS 14 或 iOS 13,则可以使用模仿 Apple [=27] 的 Focuser 库=].
https://github.com/art-technologies/swift-focuser
这是一个示例代码。您会注意到 API 看起来几乎与 Apple 完全一样,但是 Focuser 还提供使用键盘将第一响应者移动到链下,这非常方便。
如果您对 @JoshuaKifer 或 @ahaze 的回复有任何疑问,
我通过 在父 class 上使用修饰符而不是在 TextField 本身上解决了我的问题。
我在做什么:
TextField("Placeholder text...", text: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
我是如何解决问题的:
YourParentStruct(searchInput: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
为了清楚起见,我将把父结构的定义放在下面
struct YourParentStruct: View {
@Binding var searchInput: String
var body: some View {
HStack {
TextField("Placeholder text...", text: $searchInput)
.padding()
.background(Color.gray)
.cornerRadius(10)
}
}
}
在最简单 形式 iOS 13(不使用任何第三方 sdk/repo ,或者如果您还没有升级到 iOS 14 以使用 focus
修饰符)
struct CustomTextField: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
UITextField(frame: .zero)
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.becomeFirstResponder()
}
}
用法:
struct ContentView : View {
var body: some View {
CustomTextField()
.frame(width: 300, height: 50)
.background(Color.red)
}
}
SwiftUI
struct ContentView: View {
enum Field {
case firstTextfield
case secondTextfield
case lastTextfield
}
@State private var firstTextfield = ""
@State private var secondTextfield = ""
@State private var lastTextfield = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Enter anything on first textfield", text: $firstTextfield)
.focused($focusedField, equals: .firstTextfield)
.submitLabel(.next)
TextField("Enter anything on second textfield", text: $secondTextfield)
.focused($focusedField, equals: .secondTextfield)
.submitLabel(.next)
TextField("Enter anything on last textfield", text: $lastTextfield)
.focused($focusedField, equals: .lastTextfield)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstTextfield:
focusedField = .secondTextfield
case .secondTextfield:
focusedField = .lastTextfield
default:
focusedField = nil
}
}
}
}
描述:添加一个带有文本字段案例的枚举,以及一个 属性 包装在具有该枚举类型的 @FocusState
中。添加 focused(_:equals:)
修饰符以具有绑定值,等于枚举案例。现在,您可以将 focusedField
更改为您希望光标所在的任何文本字段,或者通过将 nil 分配给 focusedField 来退出第一响应者。
这是我的 SwiftUI
代码:
struct ContentView : View {
@State var showingTextField = false
@State var text = ""
var body: some View {
return VStack {
if showingTextField {
TextField($text)
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
我想要的是当文本字段变得可见时,使文本字段成为第一响应者(即接收焦点并弹出键盘)。
Swift UI 3
从 Xcode 13 开始,您可以使用 focused
修饰符使视图成为第一响应者。
Swift UI 1/2
目前似乎不可能,但您可以自己实现类似的东西。
您可以创建自定义文本字段并添加一个值以使其成为第一响应者。
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
var didBecomeFirstResponder = false
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
@Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
注意:需要 didBecomeFirstResponder
来确保文本字段仅成为第一响应者一次,而不是在 SwiftUI
每次刷新时!
你会像这样使用它...
struct ContentView : View {
@State var text: String = ""
var body: some View {
CustomTextField(text: $text, isFirstResponder: true)
.frame(width: 300, height: 50)
.background(Color.red)
}
}
P.S。我添加了一个 frame
,因为它的行为不像股票 TextField
,这意味着幕后还有更多事情要做。
更多关于 Coordinators
的精彩 WWDC 19 演讲:
Integrating SwiftUI
测试于 Xcode 11.4
对于最终来到这里但使用@Matteo Pacini 的回答遇到崩溃的任何人,请注意 beta 4 中的此更改:
init(text: Binding<String>) {
$text = text
}
应该使用:
init(text: Binding<String>) {
_text = text
}
并且如果您想让文本字段成为 sheet
中的第一响应者,请注意在显示文本字段之前您不能调用 becomeFirstResponder
。换句话说,将@Matteo Pacini 的文本字段直接放在 sheet
内容中会导致崩溃。
要解决此问题,请额外检查 uiView.window != nil
文本字段的可见性。只有在视图层次结构中之后才关注:
struct AutoFocusTextField: UIViewRepresentable {
@Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context:
UIViewRepresentableContext<AutoFocusTextField>) {
uiView.text = text
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: AutoFocusTextField
init(_ autoFocusTextField: AutoFocusTextField) {
self.parent = autoFocusTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
iOS 15
有一个名为 @FocusState
的新包装器,用于控制键盘和聚焦键盘的状态('aka' firstResponder)。
成为第一响应者(专注)
如果您在文本字段上使用 focused
修饰符,您可以使它们成为焦点:
辞去第一响应者(关闭键盘)
或通过将变量设置为 nil
:
iOS 13 岁及以上:老了但还能工作!
简单的包装器结构 - 像本地人一样工作:
请注意 根据评论中的要求添加了文本绑定支持
struct LegacyTextField: UIViewRepresentable {
@Binding public var isFirstResponder: Bool
@Binding public var text: String
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, configuration: @escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
configuration(uiView)
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
@objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
用法:
struct ContentView: View {
@State var text = ""
@State var isFirstResponder = false
var body: some View {
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder)
}
}
奖励:完全可定制
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
[=12=].textColor = .red
[=12=].tintColor = .blue
}
此方法完全适用。例如,您可以看到
使用SwiftUI-Introspect,您可以:
TextField("", text: $value)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
所选答案导致 AppKit 出现无限循环问题。我不知道 UIKit 案例。
为避免该问题,我建议直接共享 NSTextField
实例。
import AppKit
import SwiftUI
struct Sample1: NSViewRepresentable {
var textField: NSTextField
func makeNSView(context:NSViewRepresentableContext<Sample1>) -> NSView { textField }
func updateNSView(_ x:NSView, context:NSViewRepresentableContext<Sample1>) {}
}
你可以像这样使用它。
let win = NSWindow()
let txt = NSTextField()
win.setIsVisible(true)
win.setContentSize(NSSize(width: 256, height: 256))
win.center()
win.contentView = NSHostingView(rootView: Sample1(textField: txt))
win.makeFirstResponder(txt)
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.run()
这打破了纯值语义,但依赖于 AppKit 意味着您部分放弃了纯值语义并会带来一些不便。这是我们现在需要的一个魔法洞,用于处理 SwiftUI 中缺乏第一响应者控制的问题。
当我们直接访问 NSTextField
时,设置第一响应者是普通的 AppKit 方式,因此没有可见的麻烦源。
您可以下载工作源代码 here。
正如其他人所指出的(例如
import Cocoa
import SwiftUI
class FirstResponderNSSearchFieldController: NSViewController {
@Binding var text: String
init(text: Binding<String>) {
self._text = text
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let searchField = NSSearchField()
searchField.delegate = self
self.view = searchField
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
}
}
extension FirstResponderNSSearchFieldController: NSSearchFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.text = textField.stringValue
}
}
}
struct FirstResponderNSSearchFieldRepresentable: NSViewControllerRepresentable {
@Binding var text: String
func makeNSViewController(
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) -> FirstResponderNSSearchFieldController {
return FirstResponderNSSearchFieldController(text: $text)
}
func updateNSViewController(
_ nsViewController: FirstResponderNSSearchFieldController,
context: NSViewControllerRepresentableContext<FirstResponderNSSearchFieldRepresentable>
) {
}
}
示例 SwiftUI 视图:
struct ContentView: View {
@State private var text: String = ""
var body: some View {
FirstResponderNSSearchFieldRepresentable(text: $text)
}
}
由于无法通过 SwiftUI 使用 Responder Chain,因此我们必须使用 UIViewRepresentable 使用它。
请看下面的 link,因为我已经做了一个解决方法,它的工作方式与我们过去使用 UIKit 的方式类似。
这是我的实现变体,基于@Mojtaba Hosseini 和@Matteo Pacini 解决方案。 我对 SwiftUI 还是个新手,所以我不能保证代码的绝对正确性,但它确实有效。
希望对大家有所帮助。
ResponderView:这是一个通用的响应者视图,可以与任何 UIKit 视图一起使用。
struct ResponderView<View: UIView>: UIViewRepresentable {
@Binding var isFirstResponder: Bool
var configuration = { (view: View) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> View { View() }
func makeCoordinator() -> Coordinator {
Coordinator($isFirstResponder)
}
func updateUIView(_ uiView: View, context: UIViewRepresentableContext<Self>) {
context.coordinator.view = uiView
_ = isFirstResponder ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
configuration(uiView)
}
}
// MARK: - Coordinator
extension ResponderView {
final class Coordinator {
@Binding private var isFirstResponder: Bool
private var anyCancellable: AnyCancellable?
fileprivate weak var view: UIView?
init(_ isFirstResponder: Binding<Bool>) {
_isFirstResponder = isFirstResponder
self.anyCancellable = Publishers.keyboardHeight.sink(receiveValue: { [weak self] keyboardHeight in
guard let view = self?.view else { return }
DispatchQueue.main.async { self?.isFirstResponder = view.isFirstResponder }
})
}
}
}
// MARK: - keyboardHeight
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { ([=10=].userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
struct ResponderView_Previews: PreviewProvider {
static var previews: some View {
ResponderView<UITextField>.init(isFirstResponder: .constant(false)) {
[=10=].placeholder = "Placeholder"
}.previewLayout(.fixed(width: 300, height: 40))
}
}
ResponderTextField - 这是一个方便的 ResponderView 文本字段包装器。
struct ResponderTextField: View {
var placeholder: String
@Binding var text: String
@Binding var isFirstResponder: Bool
private var textFieldDelegate: TextFieldDelegate
init(_ placeholder: String, text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.placeholder = placeholder
self._text = text
self._isFirstResponder = isFirstResponder
self.textFieldDelegate = .init(text: text)
}
var body: some View {
ResponderView<UITextField>(isFirstResponder: $isFirstResponder) {
[=11=].text = self.text
[=11=].placeholder = self.placeholder
[=11=].delegate = self.textFieldDelegate
}
}
}
// MARK: - TextFieldDelegate
private extension ResponderTextField {
final class TextFieldDelegate: NSObject, UITextFieldDelegate {
@Binding private(set) var text: String
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
}
struct ResponderTextField_Previews: PreviewProvider {
static var previews: some View {
ResponderTextField("Placeholder",
text: .constant(""),
isFirstResponder: .constant(false))
.previewLayout(.fixed(width: 300, height: 40))
}
}
以及使用方法。
struct SomeView: View {
@State private var login: String = ""
@State private var password: String = ""
@State private var isLoginFocused = false
@State private var isPasswordFocused = false
var body: some View {
VStack {
ResponderTextField("Login", text: $login, isFirstResponder: $isLoginFocused)
ResponderTextField("Password", text: $password, isFirstResponder: $isPasswordFocused)
}
}
}
这是一个适用于 introspect 的 ViewModifier。它适用于 AppKit MacOS,Xcode 11.5
struct SetFirstResponderTextField: ViewModifier {
@State var isFirstResponderSet = false
func body(content: Content) -> some View {
content
.introspectTextField { textField in
if self.isFirstResponderSet == false {
textField.becomeFirstResponder()
self.isFirstResponderSet = true
}
}
}
}
由于 SwiftUI 2 不支持第一响应者,但我使用此解决方案。它很脏,但可能适用于只有 1 UITextField
和 1 UIWindow
.
import SwiftUI
struct MyView: View {
@Binding var text: String
var body: some View {
TextField("Hello world", text: $text)
.onAppear {
UIApplication.shared.windows.first?.rootViewController?.view.textField?.becomeFirstResponder()
}
}
}
private extension UIView {
var textField: UITextField? {
subviews.compactMap { [=10=] as? UITextField }.first ??
subviews.compactMap { [=10=].textField }.first
}
}
要填补这个缺失的功能,您可以使用 Swift 包管理器安装 SwiftUIX:
- 在 Xcode 中,打开您的项目并导航至文件 → Swift 包 → 添加包依赖项...
- 粘贴存储库 URL (https://github.com/SwiftUIX/SwiftUIX) 并单击“下一步”。
- 对于规则,select 分支(分支设置为主)。
- 单击“完成”。
- 打开项目设置,将SwiftUI.framework添加到链接框架和库,将状态设置为可选。
更多信息:https://github.com/SwiftUIX/SwiftUIX
import SwiftUI
import SwiftUIX
struct ContentView : View {
@State var showingTextField = false
@State var text = ""
var body: some View {
return VStack {
if showingTextField {
CocoaTextField("Placeholder text", text: $text)
.isFirstResponder(true)
.frame(width: 300, height: 48, alignment: .center)
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
我们有一个解决方案,可以毫不费力地控制第一响应者。
https://github.com/mobilinked/MbSwiftUIFirstResponder
TextField("Name", text: $name)
.firstResponder(id: FirstResponders.name, firstResponder: $firstResponder, resignableUserOperations: .all)
TextEditor(text: $notes)
.firstResponder(id: FirstResponders.notes, firstResponder: $firstResponder, resignableUserOperations: .all)
响应链
我在 iOS 13+
上为 跨平台第一响应者处理制作了这个小包,没有子类化视图或在 SwiftUI 中制作自定义 ViewRepresentableshttps://github.com/Amzd/ResponderChain
如何将其应用于您的问题
SceneDelegate.swift
...
// Set the ResponderChain as environmentObject
let rootView = ContentView().environmentObject(ResponderChain(forWindow: window))
...
ContentView.swift
struct ContentView: View {
@EnvironmentObject var chain: ResponderChain
@State var showingTextField = false
@State var text = ""
var body: some View {
return VStack {
if showingTextField {
TextField($text).responderTag("field1").onAppear {
DispatchQueue.main.async {
chain.firstResponder = "field1"
}
}
}
Button(action: { self.showingTextField.toggle() }) {
Text ("Show")
}
}
}
}
不是真正的答案,只是在 Casper 的伟大解决方案的基础上使用方便的修饰符构建 -
struct StartInput: ViewModifier {
@EnvironmentObject var chain: ResponderChain
private let tag: String
init(tag: String) {
self.tag = tag
}
func body(content: Content) -> some View {
content.responderTag(tag).onAppear() {
DispatchQueue.main.async {
chain.firstResponder = tag
}
}
}
}
extension TextField {
func startInput(_ tag: String = "field") -> ModifiedContent<TextField<Label>, StartInput> {
self.modifier(StartInput(tag: tag))
}
}
就像-
一样使用TextField("Enter value:", text: $quantity)
.startInput()
iOS 15.0+
macOS 12.0+,
Mac 催化剂 15.0+,
tvOS 15.0+,
watchOS 8.0+
如果您只有一个 TextField,请使用 focused(_:)
。
focused(_:)
Modifies this view by binding its focus state to the given Boolean state value.
struct NameForm: View {
@FocusState private var isFocused: Bool
@State private var name = ""
var body: some View {
TextField("Name", text: $name)
.focused($isFocused)
Button("Submit") {
if name.isEmpty {
isFocued = true
}
}
}
}
如果您有多个 TextField,请使用 focused(_:equals:)
。
focused(_:equals:)
Modifies this view by binding its focus state to the given state value.
struct LoginForm: View {
enum Field: Hashable {
case usernameField
case passwordField
}
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
handleLogin(username, password)
}
}
}
}
}
SwiftUI 文档
更新
我在 xCode version 13.0 beta 5 (13A5212g)
中对此进行了测试。有效
在我的例子中,我想立即聚焦一个文本字段,我使用了 .onappear
函数
struct MyView: View {
@FocusState private var isTitleTextFieldFocused: Bool
@State private var title = ""
var body: some View {
VStack {
TextField("Title", text: $title)
.focused($isTitleTextFieldFocused)
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isTitleTextFieldFocused = true
}
}
}
}
扩展@JoshuaKifer 上面的回答,如果您在使用 Introspect 使文本字段成为第一响应者时处理导航动画出现故障。使用这个:
import SchafKit
@State var field: UITextField?
TextField("", text: $value)
.introspectTextField { textField in
field = textField
}
.onDidAppear {
field?.becomeFirstResponder()
}
有关此解决方案的更多详细信息 here。
我采用了一些不同的方法 - 我基于 UIView
制作了它,而不是基于 UITextField
的 UIViewRepresentable
并使用 [=16= 将其插入 SwiftUI 视图层次结构]修饰符。在 UIView
中,我添加了逻辑以在子视图和父视图中找到 canBecomeFirstResponder
的第一个视图。
private struct FocusableUIView: UIViewRepresentable {
var isFirstResponder: Bool = false
class Coordinator: NSObject {
var didBecomeFirstResponder = false
}
func makeUIView(context: UIViewRepresentableContext<FocusableUIView>) -> UIView {
let view = UIView()
view.backgroundColor = .clear
return view
}
func makeCoordinator() -> FocusableUIView.Coordinator {
return Coordinator()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<FocusableUIView>) {
guard uiView.window != nil, isFirstResponder, !context.coordinator.didBecomeFirstResponder else {
return
}
var foundRepsonder: UIView?
var currentSuperview: UIView? = uiView
repeat {
foundRepsonder = currentSuperview?.subviewFirstPossibleResponder
currentSuperview = currentSuperview?.superview
} while foundRepsonder == nil && currentSuperview != nil
guard let responder = foundRepsonder else {
return
}
DispatchQueue.main.async {
responder.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
private extension UIView {
var subviewFirstPossibleResponder: UIView? {
guard !canBecomeFirstResponder else { return self }
for subview in subviews {
if let firstResponder = subview.subviewFirstPossibleResponder {
return firstResponder
}
}
return nil
}
}
这是一个如何使用它使 TextField 自动对焦的示例(+ 奖金利用 @FocusState
新 iOS 15 api)。
extension View {
@ViewBuilder
func autofocus() -> some View {
if #available(iOS 15, *) {
modifier(AutofocusedViewModifiers.Modern())
} else {
modifier(AutofocusedViewModifiers.Legacy())
}
}
}
private enum AutofocusedViewModifiers {
struct Legacy: ViewModifier {
func body(content: Content) -> some View {
content
.background(FocusableUIView(isFirstResponder: isFocused))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
@State private var isFocused = false
}
@available(iOS 15, *)
struct Modern: ViewModifier {
func body(content: Content) -> some View {
content
.focused($isFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
}
}
@FocusState private var isFocused: Bool
}
}
内容视图示例:
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
TextField("placeholder", text: $text)
Text("some text")
}
.autofocus()
}
}
正确的 SwiftUI 方法是如上所述使用 @FocusState
。但是,此 API 仅适用于 iOS 15。如果您使用的是 iOS 14 或 iOS 13,则可以使用模仿 Apple [=27] 的 Focuser 库=].
https://github.com/art-technologies/swift-focuser
这是一个示例代码。您会注意到 API 看起来几乎与 Apple 完全一样,但是 Focuser 还提供使用键盘将第一响应者移动到链下,这非常方便。
如果您对 @JoshuaKifer 或 @ahaze 的回复有任何疑问, 我通过 在父 class 上使用修饰符而不是在 TextField 本身上解决了我的问题。
我在做什么:
TextField("Placeholder text...", text: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
我是如何解决问题的:
YourParentStruct(searchInput: $searchInput)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
为了清楚起见,我将把父结构的定义放在下面
struct YourParentStruct: View {
@Binding var searchInput: String
var body: some View {
HStack {
TextField("Placeholder text...", text: $searchInput)
.padding()
.background(Color.gray)
.cornerRadius(10)
}
}
}
在最简单 形式 iOS 13(不使用任何第三方 sdk/repo ,或者如果您还没有升级到 iOS 14 以使用 focus
修饰符)
struct CustomTextField: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
UITextField(frame: .zero)
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.becomeFirstResponder()
}
}
用法:
struct ContentView : View {
var body: some View {
CustomTextField()
.frame(width: 300, height: 50)
.background(Color.red)
}
}
SwiftUI
struct ContentView: View {
enum Field {
case firstTextfield
case secondTextfield
case lastTextfield
}
@State private var firstTextfield = ""
@State private var secondTextfield = ""
@State private var lastTextfield = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("Enter anything on first textfield", text: $firstTextfield)
.focused($focusedField, equals: .firstTextfield)
.submitLabel(.next)
TextField("Enter anything on second textfield", text: $secondTextfield)
.focused($focusedField, equals: .secondTextfield)
.submitLabel(.next)
TextField("Enter anything on last textfield", text: $lastTextfield)
.focused($focusedField, equals: .lastTextfield)
.submitLabel(.join)
}
.onSubmit {
switch focusedField {
case .firstTextfield:
focusedField = .secondTextfield
case .secondTextfield:
focusedField = .lastTextfield
default:
focusedField = nil
}
}
}
}
描述:添加一个带有文本字段案例的枚举,以及一个 属性 包装在具有该枚举类型的 @FocusState
中。添加 focused(_:equals:)
修饰符以具有绑定值,等于枚举案例。现在,您可以将 focusedField
更改为您希望光标所在的任何文本字段,或者通过将 nil 分配给 focusedField 来退出第一响应者。