【问题标题】:How to move to next TextField in SwiftUI?如何在 SwiftUI 中移动到下一个 TextField?
【发布时间】:2020-02-28 13:45:19
【问题描述】:

使用 Swift5.1.2、iOS13.2、Xcode-11.2,

在 Stackview 中有多个 TextField,我想在用户在第一个 TextField 中键入 x 个字符后立即移至下一个 TextField。

使用this link,我可以识别 TextField 条目何时达到 x 个字符数。但是,我不知道如何让 firstResponder 跳转到 StackView 中的第二个 TextField。

SwiftUI 有解决方案吗?

【问题讨论】:

  • 我没有用 SwiftUI 编写的完整答案,但我会首先使用 stackView.arrangedSubviews.firstIndex(of: view) 获取 StackView 中当前 firstResponder 的索引。然后,获取found index + 1 的排列子视图,并使用becomeFirstResponder() 制作该firstResponder。
  • 在 iOS 15 中,我们现在可以使用 @FocusState 来控制应该关注哪个字段 - 请参阅 this answer

标签: swift textfield swiftui first-responder


【解决方案1】:

iOS 15+

使用@FocusState

iOS 15 之前

我接受了@Philip Borbon 的回答并稍微清理了一下。我删除了很多自定义内容,并保持在最低限度,以便更容易查看所需内容。

struct CustomTextfield: UIViewRepresentable {
    let label: String
    @Binding var text: String
    
    var focusable: Binding<[Bool]>? = nil
    
    var returnKeyType: UIReturnKeyType = .default
    
    var tag: Int? = nil
    
    var onCommit: (() -> Void)? = nil
    
    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField(frame: .zero)
        textField.placeholder = label
        textField.delegate = context.coordinator
        
        textField.returnKeyType = returnKeyType
        
        if let tag = tag {
            textField.tag = tag
        }
        
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
        
        if let focusable = focusable?.wrappedValue {
            var resignResponder = true
            
            for (index, focused) in focusable.enumerated() {
                if uiView.tag == index && focused {
                    uiView.becomeFirstResponder()
                    resignResponder = false
                    break
                }
            }
            
            if resignResponder {
                uiView.resignFirstResponder()
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    final class Coordinator: NSObject, UITextFieldDelegate {
        let parent: CustomTextfield
        
        init(_ parent: CustomTextfield) {
            self.parent = parent
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            guard var focusable = parent.focusable?.wrappedValue else { return }
            
            for i in 0...(focusable.count - 1) {
                focusable[i] = (textField.tag == i)
            }
            parent.focusable?.wrappedValue = focusable
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            guard var focusable = parent.focusable?.wrappedValue else {
                textField.resignFirstResponder()
                return true
            }
            
            for i in 0...(focusable.count - 1) {
                focusable[i] = (textField.tag + 1 == i)
            }
            
            parent.focusable?.wrappedValue = focusable
            
            if textField.tag == focusable.count - 1 {
                textField.resignFirstResponder()
            }
            
            return true
        }
        
        @objc func textFieldDidChange(_ textField: UITextField) {
            parent.text = textField.text ?? ""
        }
    }
}

【讨论】:

【解决方案2】:

我相信最终在 iOS 15 中会有一个真正的 SwiftUI 解决方案来解决这个问题。

had this problemwrote and article about it,因为我找不到。

基本上你可以创建一些东西来完成这个:

  • 焦点对象:观察到的可识别对象或用于焦点的数组索引 int 变量
  • Focusable Objects Array:与您希望作为 First Responders 迭代的 TextField 关联的可识别对象数组
  • TextFieldWrapper:管理每个 TextField 的 FocusState 并更新焦点对象的对象(见第一个项目符号)

然后将闭包或函数引用传递给 TextField Wrapper 对象,以允许它更新数组中的焦点对象。我会使用某种视图模型,也许是 FocusStateViewModel。您可以制作更复杂的解决方案以满足您的需求from this gist

或者,这是解决方案的最小复制:

import SwiftUI

struct MyObject: Identifiable, Equatable {
    var id: String
    public var value: String
    init(name: String, value: String) {
        self.id = name
        self.value = value
    }
}

struct ContentView: View {

    @State var myObjects: [MyObject] = [
        MyObject(name: "aa", value: "1"),
        MyObject(name: "bb", value: "2"),
        MyObject(name: "cc", value: "3"),
        MyObject(name: "dd", value: "4")
    ]
    @State var focus: MyObject?

    var body: some View {
        ScrollView(.vertical) {
            VStack {
                Text("Header")
                ForEach(self.myObjects) { obj in
                    Divider()
                    FocusField(displayObject: obj, focus: $focus, nextFocus: {
                        guard let index = self.myObjects.firstIndex(of: $0) else {
                            return
                        }
                        self.focus = myObjects.indices.contains(index + 1) ? myObjects[index + 1] : nil
                    })
                }
                Divider()
                Text("Footer")
            }
        }
    }
}

struct FocusField: View {

    @State var displayObject: MyObject
    @FocusState var isFocused: Bool
    @Binding var focus: MyObject?
    var nextFocus: (MyObject) -> Void

    var body: some View {
    TextField("Test", text: $displayObject.value)
            .onChange(of: focus, perform: { newValue in
                self.isFocused = newValue == displayObject
            })
            .focused(self.$isFocused)
            .submitLabel(.next)
            .onSubmit {
                self.nextFocus(displayObject)
            }
    }
}

【讨论】:

    【解决方案3】:

    如果您可以使用 iOS 15,Mojtaba 提出的解决方案非常棒。由于大多数项目必须支持旧版本的 iOS,因此它不起作用。但是,如果您使用的是 iOS 13 或 iOS 14,则可以使用 Focuser 库,它提供的正是它。

    您可以从 Github 下载示例项目以查看示例。然而,API 被建模为在 iOS 15 中工作。

    【讨论】:

      【解决方案4】:

      iOS 15

      在 iOS 15 中,我们现在可以使用 @FocusState 来控制应该关注哪个字段。

      这是一个演示:

      struct ContentView: View {
          @State private var street: String = ""
          @State private var city: String = ""
          @State private var country: String = ""
      
          @FocusState private var focusedField: Field?
      
          var body: some View {
              NavigationView {
                  VStack {
                      TextField("Street", text: $street)
                          .focused($focusedField, equals: .street)
                      TextField("City", text: $city)
                          .focused($focusedField, equals: .city)
                      TextField("Country", text: $country)
                          .focused($focusedField, equals: .country)
                  }
                  .toolbar {
                      ToolbarItem(placement: .keyboard) {
                          Button(action: focusPreviousField) {
                              Image(systemName: "chevron.up")
                          }
                          .disabled(!canFocusPreviousField()) // remove this to loop through fields
                      }
                      ToolbarItem(placement: .keyboard) {
                          Button(action: focusNextField) {
                              Image(systemName: "chevron.down")
                          }
                          .disabled(!canFocusNextField()) // remove this to loop through fields
                      }
                  }
              }
          }
      }
      
      extension ContentView {
          private enum Field: Int, CaseIterable {
              case street, city, country
          }
          
          private func focusPreviousField() {
              focusedField = focusedField.map {
                  Field(rawValue: $0.rawValue - 1) ?? .country
              }
          }
      
          private func focusNextField() {
              focusedField = focusedField.map {
                  Field(rawValue: $0.rawValue + 1) ?? .street
              }
          }
          
          private func canFocusPreviousField() -> Bool {
              guard let currentFocusedField = focusedField else {
                  return false
              }
              return currentFocusedField.rawValue > 0
          }
      
          private func canFocusNextField() -> Bool {
              guard let currentFocusedField = focusedField else {
                  return false
              }
              return currentFocusedField.rawValue < Field.allCases.count - 1
          }
      }
      

      注意:从 Xcode 13 beta 1 开始,@FocusStateForm/List 中不起作用。这应该在下一个版本中修复。

      【讨论】:

        【解决方案5】:

        iOS 15

        今年,Apple 引入了一个新的修饰符以及一个名为 @FocusState 的新包装器,用于控制键盘和焦点键盘的状态('aka' firstResponder)。

        以下是如何迭代 textFields 的示例:

        此外,您可以查看 this answer 了解如何创建 textField 第一响应者或将其辞职以隐藏键盘,并了解有关如何将此枚举绑定到 textFields 的更多信息.

        【讨论】:

          【解决方案6】:

          我正在使用UITextFieldUIViewRepresentable 来实现这一目标。

          定义每个文本字段的tag,并声明一个布尔值列表,该列表具有相同数量的可用文本字段以作为返回键的焦点fieldFocus,它将根据当前索引跟踪下一个要关注的文本字段/标签。

          用法:

          import SwiftUI
          
          struct Sample: View {
              @State var firstName: String = ""
              @State var lastName: String = ""
              
              @State var fieldFocus = [false, false]
              
              var body: some View {
                  VStack {
                      KitTextField (
                          label: "First name",
                          text: $firstName,
                          focusable: $fieldFocus,
                          returnKeyType: .next,
                          tag: 0
                      )
                      .padding()
                      .frame(height: 48)
                      
                      KitTextField (
                          label: "Last name",
                          text: $lastName,
                          focusable: $fieldFocus,
                          returnKeyType: .done,
                          tag: 1
                      )
                      .padding()
                      .frame(height: 48)
                  }
              }
          }
          

          UITextField in UIViewRepresentable:

          import SwiftUI
          
          struct KitTextField: UIViewRepresentable {
              let label: String
              @Binding var text: String
              
              var focusable: Binding<[Bool]>? = nil
              var isSecureTextEntry: Binding<Bool>? = nil
              
              var returnKeyType: UIReturnKeyType = .default
              var autocapitalizationType: UITextAutocapitalizationType = .none
              var keyboardType: UIKeyboardType = .default
              var textContentType: UITextContentType? = nil
              
              var tag: Int? = nil
              var inputAccessoryView: UIToolbar? = nil
              
              var onCommit: (() -> Void)? = nil
              
              func makeUIView(context: Context) -> UITextField {
                  let textField = UITextField(frame: .zero)
                  textField.delegate = context.coordinator
                  textField.placeholder = label
                  
                  textField.returnKeyType = returnKeyType
                  textField.autocapitalizationType = autocapitalizationType
                  textField.keyboardType = keyboardType
                  textField.isSecureTextEntry = isSecureTextEntry?.wrappedValue ?? false
                  textField.textContentType = textContentType
                  textField.textAlignment = .left
                  
                  if let tag = tag {
                      textField.tag = tag
                  }
                  
                  textField.inputAccessoryView = inputAccessoryView
                  textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
                  
                  textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
                  
                  return textField
              }
              
              func updateUIView(_ uiView: UITextField, context: Context) {
                  uiView.text = text
                  uiView.isSecureTextEntry = isSecureTextEntry?.wrappedValue ?? false
                  
                  if let focusable = focusable?.wrappedValue {
                      var resignResponder = true
                      
                      for (index, focused) in focusable.enumerated() {
                          if uiView.tag == index && focused {
                              uiView.becomeFirstResponder()
                              resignResponder = false
                              break
                          }
                      }
                      
                      if resignResponder {
                          uiView.resignFirstResponder()
                      }
                  }
              }
              
              func makeCoordinator() -> Coordinator {
                  Coordinator(self)
              }
              
              final class Coordinator: NSObject, UITextFieldDelegate {
                  let control: KitTextField
                  
                  init(_ control: KitTextField) {
                      self.control = control
                  }
                  
                  func textFieldDidBeginEditing(_ textField: UITextField) {
                      guard var focusable = control.focusable?.wrappedValue else { return }
                      
                      for i in 0...(focusable.count - 1) {
                          focusable[i] = (textField.tag == i)
                      }
                      
                      control.focusable?.wrappedValue = focusable
                  }
                  
                  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
                      guard var focusable = control.focusable?.wrappedValue else {
                          textField.resignFirstResponder()
                          return true
                      }
                      
                      for i in 0...(focusable.count - 1) {
                          focusable[i] = (textField.tag + 1 == i)
                      }
                      
                      control.focusable?.wrappedValue = focusable
                      
                      if textField.tag == focusable.count - 1 {
                          textField.resignFirstResponder()
                      }
                      
                      return true
                  }
                  
                  func textFieldDidEndEditing(_ textField: UITextField) {
                      control.onCommit?()
                  }
                  
                  @objc func textFieldDidChange(_ textField: UITextField) {
                      control.text = textField.text ?? ""
                  }
              }
          }
          

          【讨论】:

          • 这真的很酷,你能做一个更简单的版本吗?我正在尝试实施您的解决方案,但我不确定所需的绑定。即使添加一些文档也会很好。
          • 移动到下一个字段所需的参数是labeltextfocusabletag。我只是有其他参数来公开一些UITextField 属性来自定义它或根据我的使用情况,您可以省略或公开您需要或不需要的属性。
          • @xTwisteDx 我已经发布了一个答案,让这个解决方案更简单一些。
          【解决方案7】:

          我能够通过 Introspect 库完成这项工作。 https://github.com/siteline/SwiftUI-Introspect:

                        @State private var passcode = ""
          
                        HStack {
                            TextField("", text: self.$passcode)
                              .introspectTextField { textField in
                                if self.passcode.count >= 1 {
                                  textField.resignFirstResponder()
                                } else if self.passcode.count < 1 {
                                  textField.becomeFirstResponder()
                                }
                            }
                            TextField("", text: self.$passcode)
                              .introspectTextField { textField in
                                if self.passcode.count >= 2
                                  textField.resignFirstResponder()
                                } else if self.passcode.count < 2 {
                                  textField.becomeFirstResponder()
                                }
                            }
                        }
          

          我可能因为试图复制和粘贴我的代码而搞砸了实现,但你知道它是如何工作的。

          【讨论】:

          • 这不会按预期工作,因为它会在用户输入时跳转到下一个字段。这令人困惑。
          【解决方案8】:

          试试这个:

          import SwiftUI
          
          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 entry = ""
              @State private var entry2 = ""
          
              let characterLimit = 6
          
              var body: some View {
                  VStack {
                      TextField("hallo", text: $entry)
                          .disabled(entry.count > (characterLimit - 1))
          
                      ResponderTextField(isFirstResponder: entry.count > (characterLimit - 1)) { uiView in
                          uiView.placeholder = "2nd textField"
                      }
                  }
              }
          }
          

          【讨论】:

          • 至少它有一个投票支持 ;) 是的,当我写它时它起作用了
          • @Learn2Code,它有效。只需将他/她的代码插入视图并自己查看即可。但是它会从第一个文本字段移动到第二个文本字段,但看不到如何移动第三个、第四个 ...
          • 别管第 3 次,第 4 次了……它很容易修改以使其工作:)
          • @GrandSteph 你是如何修改它以适应第三和第四个文本字段的?我需要 6 个...
          • @Jack 我最终没有使用此代码。相反,我使用了类似于 this SATexfield 的东西,因为我发现它更简单、更通用、更干净
          猜你喜欢
          • 2020-01-27
          • 2020-02-13
          • 1970-01-01
          • 1970-01-01
          • 2015-05-30
          • 1970-01-01
          • 1970-01-01
          • 2019-10-22
          • 1970-01-01
          相关资源
          最近更新 更多