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

时间:2019-06-08 15:44:30

标签: swift 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")
            }
        }
    }
}

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

17 个答案:

答案 0 :(得分:9)

对于那些最终在这里遇到但使用@Matteo Pacini的答案崩溃的人,请注意Beta 4:Cannot assign to property: '$text' is immutable中有关此阻止的更改:

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

答案 1 :(得分:8)

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

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

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

答案 2 :(得分:6)

正如其他人所指出的(例如@kipelovets comment on the accepted answer,例如@Eonil's answer),我还没有找到任何可在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)
  }
}

答案 3 :(得分:4)

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

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

答案 4 :(得分:3)

简单的包装器结构-类似于本机:

struct ResponderTextField: UIViewRepresentable {

    typealias TheUIView = UITextField
    var isFirstResponder: Bool
    var configuration = { (view: TheUIView) in }

    func makeUIView(context: UIViewRepresentableContext<Self>) -> TheUIView { TheUIView() }
    func updateUIView(_ uiView: TheUIView, context: UIViewRepresentableContext<Self>) {
        _ = isFirstResponder ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
        configuration(uiView)
    }
}

用法:

struct ContentView: View {
    @State private var isActive = false

    var body: some View {
        ResponderTextField(isFirstResponder: isActive)
        // Just set `isActive` anyway you like
    }
}

?奖励:完全可定制

ResponderTextField(isFirstResponder: isActive) {
    $0.textColor = .red
    $0.tintColor = .blue
}

此方法完全适用。例如,您可以使用相同的方法How to add an Activity indicator in SwiftUI

来查看here

答案 5 :(得分:3)

iOS 15.0+

macOS 12.0+, Mac 催化剂 15.0+, tvOS 15.0+, watchOS 8.0+

如果您只有一个 TextField,请使用 focused(_:)

<块引用>

专注(_:)

通过将其焦点状态绑定到给定的 Boolean 状态值来修改此视图。

struct NameForm: View {
    
    @FocusState private var isFocued: Bool
    
    @State private var name = ""
    
    var body: some View {
        TextField("Name", text: $name)
            .focused($isFocued)
        
        Button("Submit") {
            if name.isEmpty {
                isFocued = true
            }
        }
    }
}

如果您有多个 TextField,请使用 focused(_:equals:)

<块引用>

专注(_:equals:)

通过将其焦点状态绑定到给定的状态值来修改此视图。

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 文档

答案 6 :(得分:2)

不是真正的答案,只是建立在带有方便修饰符的 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))
    }
}

就用like -

TextField("Enter value:", text: $quantity)
    .startInput()

答案 7 :(得分: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方法,因此没有明显的麻烦源。

您可以下载有效的源代码here

答案 8 :(得分:1)

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

  1. 在Xcode中,打开您的项目,然后导航至“文件”→“ Swift软件包”→“添加软件包依赖性...”。
  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")
            }
        }
    }
}

答案 9 :(得分:0)

扩展@Jos​​huaKifer 上面的答案,如果您在使用 Introspect 制作文本字段第一响应者时处理导航动画出现故障。使用这个:

import SchafKit

@State var field: UITextField?

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

有关此解决方案的更多详细信息 here

答案 10 :(得分:0)

我制作了这个小包,用于跨平台的第一响应者处理,而无需在SwiftUI中子类化视图或创建自定义ViewRepresentables

https://github.com/Amzd/ResponderChain

示例

SceneDelegate.swift

...
// Set the ResponderChain as environmentObject
let rootView = ResponderChainExample().environmentObject(ResponderChain(forWindow: window))
...

ResponderChainExample.swift

struct ResponderChainExample: View {
    @EnvironmentObject var chain: ResponderChain
    
    var body: some View {
        VStack(spacing: 20) {
            // Show which view is first responder
            Text("Selected field: \(chain.firstResponder?.description ?? "Nothing selected")")
            
            // Some views that can become first responder
            TextField("0", text: .constant("")).responderTag("0")
            TextField("1", text: .constant("")).responderTag("1")
            TextField("2", text: .constant("")).responderTag("2")
            TextField("3", text: .constant("")).responderTag("3")
            
            // Buttons to change first responder
            HStack {
                Button("Select 0", action: { chain.firstResponder = "0" })
                Button("Select 1", action: { chain.firstResponder = "1" })
                Button("Select 2", action: { chain.firstResponder = "2" })
                Button("Select 3", action: { chain.firstResponder = "3" })
            }
        }
        .padding()
        .onAppear {
            // Example how to set first responder on appear
            DispatchQueue.main.async {
                chain.firstResponder = "0"
            }
        }
    }
}

答案 11 :(得分:0)

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

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)

答案 12 :(得分:0)

这是与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
            }
         }
      }
   }

答案 13 :(得分: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)
        }
    }
}

答案 14 :(得分:0)

由于响应链无法通过SwiftUI使用,因此我们必须使用UIViewRepresentable来使用它。

请查看下面的链接,因为我做了一个变通方法,其工作原理与我们使用UIKit的方法类似。

https://stackoverflow.com/a/61121199/6445871

答案 15 :(得分:-1)

就我而言,我想立即将文本字段聚焦,我使用了 .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
            }
            
        }
    }
}

答案 16 :(得分:-2)

由于 SwiftUI 2 不支持第一响应者,但我使用了此解决方案。它很脏,但是当您只有1 Pattern和1 UITextField时,在某些用例中可能会起作用。

UIWindow