SwiftUI:如何使TextField成为第一响应者?


77

这是我的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")
            }
        }
    }
}

我想要的是当文本字段变为可见时,使文本字段成为第一响应者(即获得焦点并弹出键盘)。

Answers:


62

目前似乎还不可能,但是您可以自己实现类似的功能。

您可以创建一个自定义文本字段并添加一个值,使其成为第一响应者。

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)
    }

}

附注:我添加了一个,frame因为它的行为不像股票TextField,这意味着幕后还有更多的事情要做。

更多关于Coordinators在这个优秀的WWDC 19讲: 整合SwiftUI

在Xcode 11.4上测试


1
@MatteoPacini在Mac上似乎不起作用,您知道是否有任何可行的解决方案?
kipelovets

@kipelovets在启动包含此自定义文本字段的模态之前,先在活动视图中单击任何元素时起作用。
stardust4891

我只是遇到了这个问题,在textfield.becomeFirstResponder()内部调用override func layoutSubviews导致我的应用进入无限循环。当我尝试使用一个新的ViewController时也发生了这种情况,该ViewController也有一个textView试图在layoutSubviews内部成为becomFirstResponder。绝对是一个需要注意的偷偷摸摸的错误!
LyteSpeed

1
@Matteo还需要吗?想知道SwiftUI是否支持以编程方式将焦点设置到特定的TextField吗?
格雷格

3
@Greg是的,仍然需要。
罗马

61

使用https://github.com/timbersoftware/SwiftUI-Introspect,您可以:

TextField("", text: $value)
.introspectTextField { textField in
    textField.becomeFirstResponder()
}


1
非常感谢你的帮助。很高兴知道它也可以与SecureField()一起使用。
Tristan Clet

2
这个可可足也为我工作。我怀疑这不是可接受的答案的唯一原因是早在SwiftUI-Introspect创建之前(2019年11月)就在6月(WWDC19)提出并回答了这个问题。
dmzza

2
如果在TextField上具有样式修改器,则通常必须在样式之后添加introspectTextField修改器,因为顺序很重要。
user1909186

1
@Schnodderbalken修饰符的顺序非常重要。例如,如果您具有.padding(.all),请尝试将.introspectTextField {tf.becomeFirstResponder()中的tf}向上移动.padding(.all)。如果您有任何问题,请告诉我。
user1909186 '20

3
我无法在Introspect 0.0.6或0.1.0上使用它。:(
杰里米

20

对于那些最终在这里遇到但使用@Matteo Pacini的答案崩溃的人,请注意beta 4中的这一更改:无法分配给属性:'$ text'对于此块是不变的

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 ?? ""
        }
    }
}

我被困住了,因为我在使用shouldChangeCharactersInUITextField委托的同时还更新了中的文本updateUIView。通过更改以textFieldDidChangeSelection捕获绑定中的文本,自定义控件效果很好!谢谢。
P. Ent,

1
sheet显示输入之前尝试聚焦时确实会崩溃。您的解决方案不会崩溃,但也不会集中精力……至少在我的测试中。基本上,updateUIView仅被调用一次。
NeverwinterMoon

如果UITextfield的父视图位于动画的中间,则这种行为会很奇怪
Marwan Roushdy

如果在显示带有的工作表时另一个文本字段具有焦点,这对我来说是崩溃的AutoFocusTextField。我不得不将呼叫包装到becomeFirstResponder()一个DispatchQueue.main.async块中。
布拉德

DispatchQueue.main.async对于过渡动画,包装绝对不适合我。假设使用NavigationView并从视图1转到视图2(在其中调用了成为firstResponder的视图),并使用了异步,则过渡动画播放了两次。
NeverwinterMoon,

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.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
        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) {
    $0.textColor = .red
    $0.tintColor = .blue
}

这种方法是完全适应的。例如,你可以看到如何在SwiftUI添加活动指示灯用同样的方法在这里


1
'ResponderTextField.Type'不能转换为'(Bool,转义(ResponderTextField.TheUIView)->())-> ResponderTextField'(aka'(Bool,转义(UITextField)->())-> ResponderTextField')
stardust4891

你是在哪里拿到的?
Mojtaba Hosseini

4
这缺少TextField通常具有的$ text绑定(和标签)吗?(无论如何我都无法正常工作)
drtofu

文本字段在哪里?
Peter Schorn

1
@drtofu我添加了对绑定文本的支持。
Mojtaba Hosseini

6

正如其他人指出的那样(例如@kipelovets对已接受的答案发表评论,例如@Eonil的回答),我也没有找到任何可在macOS上使用的已接受解决方案。但是,我有些幸运,使用NSViewControllerRepresentable可以使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)
  }
}


这很棒-几乎正是我想要的。现在,我只需要弄清楚如何始终将其作为第一响应者。
bouwerp

工作得很好。谢谢!
西蒙(Simon)

3

要填充此缺少的功能,您可以使用Swift Package Manager安装SwiftUIX:

  1. 在Xcode中,打开您的项目并导航到File→Swift Packages→Add Package Dependency ...
  2. 粘贴存储库URL(https://github.com/SwiftUIX/SwiftUIX),然后单击下一步。
  3. 对于“规则”,选择“分支”(分支设置为“主”)。
  4. 单击完成。
  5. 打开项目设置,将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")
            }
        }
    }
}

2

pon响应链

我制作了这个小程序包,用于跨平台的第一个响应程序处理,而无需将视图子类化或创建自定义ViewRepresentables在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")
            }
        }
    }
}

到目前为止,我看到的最优雅的解决方案。对SwiftUI的大力支持。用过自己,强烈推荐!
Legolas Wang

您应该提到此解决方案包括对其他两个软件包的依赖。
Gene Loparco

@GeneLoparco好吧,其中之一是Introspect,无论如何我还是建议在任何SwiftUI应用程序中使用它,另一个是50行的单个文件。
Casper Zandbergen

1

选择答案会导致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方式,因此没有明显的麻烦源。

您可以在此处下载可用的源代码。


1

我们有一个解决方案,可以轻松控制第一响应者。

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)

3
请说明您的代码做什么以及如何执行;永远不要仅仅自己发布代码。
M-Chen-3

这是最好的答案-就像魅力一样!辛苦了
Gene Loparco


0

这是我的实现的变体,基于@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 { ($0.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)) {
            $0.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) {
            $0.text = self.text
            $0.placeholder = self.placeholder
            $0.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)
        }
    }
}

1
可以修改它以与macOS一起使用吗?
Peter Schorn

我会尝试:)同时,我认为此forums.developer.apple.com/thread/125339可能会对您有所帮助。
Dmytro Shvetsov

0

这是一个可以自省的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
            }
         }
      }
   }

-2

由于SwiftUI 2不支持急救人员,因此我使用了此解决方案。它很脏,但是当您只有1UITextField和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 { $0 as? UITextField }.first ??
            subviews.compactMap { $0.textField }.first
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.