【问题标题】:SwiftUI: How to make TextField become first responder?SwiftUI:如何让 TextField 成为第一响应者?
【发布时间】:2019-10-23 18:13:07
【问题描述】:

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

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

【问题讨论】:

  • 这种基本的东西甚至不被支持的事实表明 SwiftUI 目前是多么的不成熟。看看下面的解决方案,这对于应该使 GUI 编码“更简单”的东西来说是荒谬的

标签: swift textfield swiftui first-responder


【解决方案1】:

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

}

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

更多关于Coordinators 的精彩内容请参阅 WWDC 19 演讲: Integrating SwiftUI

在 Xcode 11.4 上测试

【讨论】:

  • @MatteoPacini 这似乎在 Mac 上不起作用,你知道是否有任何可行的解决方案吗?
  • @kipelovets 当您在启动包含此自定义文本字段的模式之前先单击活动视图中的任何元素时,它会起作用。
  • 我刚刚遇到了这个问题,在override func layoutSubviews 内部调用textfield.becomeFirstResponder() 导致我的应用程序进入无限循环。当我试图推动一个新的 ViewController 时发生这种情况,该 ViewController 也有一个 textView 试图在 layoutSubviews 中成为comFirstResponder。绝对是一个需要注意的偷偷摸摸的错误!
  • @Matteo 这还需要吗?想知道 SwiftUI 是否支持以编程方式将焦点设置到特定的 TextField?
  • @Greg 是的,仍然需要。
【解决方案2】:

使用SwiftUI-Introspect,您可以:

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

【讨论】:

  • 非常感谢您的帮助。很高兴知道它也适用于 SecureField()。
  • 这个 Cocoapod 也对我有用。我怀疑这不是公认答案的唯一原因是,早在 SwiftUI-Introspect 创建(2019 年 11 月下旬)之前的 6 月(WWDC19)就提出并回答了这个问题。
  • 如果您在 TextField 上有样式修饰符,则通常必须在样式后添加 introspectTextField 修饰符,因为顺序很重要。
  • 如果我有两个文本字段并且我这样做,那么一切都很酷,直到我点击第二个文本字段。一旦我点击键盘按钮,相应的字符就会添加到我作为第一响应者的第一个文本字段中。
  • 我无法让它在 Introspect 0.0.6 和 0.1.0 上运行。 :(
【解决方案3】:

iOS 15

有一个名为 @FocusState 的新包装器,它控制键盘和焦点键盘('aka' firstResponder)的状态。

成为第一响应者(专注)

如果您在文本字段上使用focused 修饰符,您可以使它们成为焦点:

辞去第一响应者(关闭键盘)

或通过将变量设置为 nil 来关闭键盘:



iOS 13 及更高版本:旧但工作正常!

简单的包装结构 - 像本地人一样工作:

请注意在 cmets 中按要求添加了文本绑定支持

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

这种方法是完全适应的。比如你可以看到How to add an Activity indicator in SwiftUI用同样的方法here

【讨论】:

  • 'ResponderTextField.Type' 不能转换为'(Bool, escaping (ResponderTextField.TheUIView) -> ()) -> ResponderTextField' (aka '(Bool, escaping (UITextField) -> () ) -> ResponderTextField')
  • 这缺少 TextField 通常具有的 $text 绑定(和标签)? (无论如何,我无法让它工作)
  • @drtofu 我添加了绑定文本支持。
  • @MojtabaHosseini 这有两个问题。 1. 如果你写了一个长句子,整个视图会在你输入时逐个字符移动到左边。 2. 配置不起作用。 TextColor 仍然是黑色。
  • @mahan 用于配置部分。在 func updateUIView(_ uiView:context:) 中添加configuration(uiView)
【解决方案4】:

对于最终来到这里但使用@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 ?? ""
        }
    }
}

【讨论】:

  • 我被卡住了,因为我使用了shouldChangeCharactersIn UITextField 委托,同时还更新了updateUIView 中的文本。通过更改为textFieldDidChangeSelection 来捕获绑定中的文本,自定义控件效果很好!谢谢。
  • 在显示输入之前尝试聚焦时确实会在sheet 中崩溃。您的解决方案不会崩溃,但也不会集中注意力……至少在我的测试中。基本上,updateUIView 只被调用一次。
  • 如果 UITextfield 的父视图位于动画中间,这会很奇怪
  • 如果在呈现带有AutoFocusTextField 的工作表时另一个文本字段具有焦点,这对我来说会崩溃。我必须将对becomeFirstResponder() 的调用封装在DispatchQueue.main.async 块中。
  • 包裹在DispatchQueue.main.async 中绝对不适合我的过渡动画。比如说,使用 NavigationView 并从视图 1 转到视图 2(调用 becomeFirstResponder 的地方),使用异步,过渡动画播放两次。
【解决方案5】:

iOS 15.0+

macOS 12.0+,

Mac Catalyst 15.0+,

tvOS 15.0+,

watchOS 8.0+

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

专注(_:)

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

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

专注(_: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 文档


更新

我在xCode version 13.0 beta 5 (13A5212g) 中对此进行了测试。有效

【讨论】:

  • 如果这只是向后兼容。至少到 iOS 14!
  • 专注于表单不适合我
  • iOS 15 或 Xcode 13 中存在错误。@Dasoga
  • 也许我只是在这里很密集,但这似乎是如何确定关注哪个领域而不是 SET 关注某个领域的答案。我正在努力弄清楚您如何为字段提供默认焦点。
【解决方案6】:

? 响应者链

我在 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")
            }
        }
    }
}

【讨论】:

  • 迄今为止我见过的最优雅的解决方案。对 SwiftUI 的大力支持。自己用过,强烈推荐!
  • 你应该提到这个解决方案包括对另外两个包的依赖。
  • 我明白这一点,但知道您的解决方案不是独立的并且存在其他代码依赖项可能对其他人有所帮助。这并不意味着解决方案是好是坏。了解依赖关系很有用,仅此而已。
  • @SomaMan 好吧,那是因为您必须再次将 environmentObjects 附加到模态。更详细的解释:medium.com/swlh/…
  • @CasperZandbergen 它非常好,但没有回答我的初始化问题。一个在最新的 SwiftUI 版本中演示它的小示例应用程序对于像我这样的新 SwiftUI 开发人员来说会有很长的路要走。我完全不熟悉 SceneDelegate 和 ApplicationDelegate 是什么,因为它们不再存在于 Apple 最新的示例代码中。我意识到 SwiftUI 本身的文档非常丰富,但我认为这只会让库更加有必要广泛记录它们的用法和假设。
【解决方案7】:

正如其他人所指出的(例如@kipelovets comment on the accepted answer,例如@Eonil's answer),我也没有找到任何可以在 macOS 上运行的公认解决方案。不过,我有一些运气,使用 NSViewControllerRepresentableNSSearchField 出现在 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)
  }
}

【讨论】:

  • 这很棒 - 几乎正是我想要的。现在我只需要弄清楚如何始终将其作为第一响应者。
  • 工作得非常好。谢谢!
  • 好主意!没想到NSViewControllerRepresentable。谢谢!
【解决方案8】:

要填补这个缺失的功能,您可以使用 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")
            }
        }
    }
}

【讨论】:

    【解决方案9】:

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

    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)
    

    【讨论】:

    • 请解释你的代码是做什么的以及它是如何做到的;永远不要自己发布代码。
    • 这是最好的答案 - 它就像一个魅力!干得好。
    【解决方案10】:

    并不是真正的答案,只是在 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()
    

    【讨论】:

    • 鉴于您的修改器是多么干净的解决方案,但需要记录它的依赖关系,我想对此表示赞同。首先,什么是异步?其次,如果需要 ResponderChain 和 Introspect,将它们显示为导入,并为 Swift 包管理器提供它们的 URL。这已经是一个热门问题了,Caspers 的答案可能在更多答案之后很难找到。
    • @SafeFastExpressive 为 Async.main 道歉——这只是我忘记删除的 DispatchQueue.async.main 的包装。关于你的第二点,我觉得我不应该指出我所指的其他人的答案的依赖关系:)
    • 你是对的。这真的不是答案——你的评论也不是。如果没有更多信息,我无法弄清楚如何使这个“优雅”的答案发挥作用。
    【解决方案11】:

    这是一个适用于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
                }
             }
          }
       }
    

    【讨论】:

      【解决方案12】:

      在我的情况下,我想立即关注一个文本字段,我使用了 .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
                  }
                  
              }
          }
      }
      

      【讨论】:

      • 嗨,这是从图书馆还是什么?
      • 这是一个原生解决方案@King
      • @King 它需要 iOS 15
      • 除了将延迟更改为 .now() + 0.02 这样键盘会立即显示之外,这个答案是最好的,至少对于 iOS 15 而言。快速简单,无需大量代码或要添加的依赖项。
      【解决方案13】:

      所选答案会导致 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

      【讨论】:

        【解决方案14】:

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

        请查看下面的链接,因为我已经制定了一种解决方法,它的工作方式与我们使用 UIKit 的方式类似。

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

        【讨论】:

          【解决方案15】:

          这是我的实现变体,基于@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)
                  }
              }
          }
          

          【讨论】:

          【解决方案16】:

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

          import SchafKit
          
          @State var field: UITextField?
          
          TextField("", text: $value)
          .introspectTextField { textField in
              field = textField
          }
          .onDidAppear {
               field?.becomeFirstResponder()
          }
          

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

          【讨论】:

            【解决方案17】:

            我采取了一些不同的方法 - 而不是基于 UITextFieldUIViewRepresentable 我基于 UIView 制作它,并使用 background 修饰符将其插入到 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()
                }
            }
            
            

            【讨论】:

              【解决方案18】:

              正确的 SwiftUI 方式是使用上面提到的@FocusState。但是,此 API 仅适用于 iOS 15。如果您使用的是 iOS 14 或 iOS 13,则可以使用按照 Apple API 建模的 Focuser 库。

              https://github.com/art-technologies/swift-focuser

              这是一个示例代码。您会注意到 API 看起来几乎与 Apple 完全一样,但是 Focuser 还提供使用键盘将第一响应者向下移动到链中,这非常方便。

              【讨论】:

                【解决方案19】:

                如果您对 @JoshuaKifer@ahaze 的回复有任何疑问, 我通过在父类上使用修饰符解决了我的问题,而不是在 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)
                           }
                       }
                   }
                

                【讨论】:

                  【解决方案20】:

                  最简单 形式在 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)
                      }
                  }
                  

                  【讨论】:

                    【解决方案21】:

                    由于 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 { $0 as? UITextField }.first ??
                                subviews.compactMap { $0.textField }.first
                        }
                    }
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2021-04-26
                      • 2023-03-24
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多