在 SwiftUI 中以编程方式自动聚焦 TextField

问题描述 投票:0回答:7

我正在使用模式将名称添加到列表中。当显示模式时,我想自动聚焦 TextField,如下所示:

我还没有找到合适的解决方案。

为了做到这一点,SwiftUI 中是否已经实现了任何东西?

感谢您的帮助。

var modal: some View {
        NavigationView{
            VStack{
                HStack{
                    Spacer()
                    TextField("Name", text: $inputText) // autofocus this!
                        .textFieldStyle(DefaultTextFieldStyle())
                        .padding()
                        .font(.system(size: 25))
                        // something like .focus() ??
                    Spacer()
                }
                Button(action: {
                    if self.inputText != ""{
                        self.players.append(Player(name: self.inputText))
                        self.inputText = ""
                        self.isModal = false
                    }
                }, label: {
                    HStack{
                        Text("Add \(inputText)")
                        Image(systemName: "plus")
                    }
                        .font(.system(size: 20))
                })
                    .padding()
                    .foregroundColor(.white)
                    .background(Color.blue)
                    .cornerRadius(10)
                Spacer()
            }
                .navigationBarTitle("New Player")
                .navigationBarItems(trailing: Button(action: {self.isModal=false}, label: {Text("Cancel").font(.system(size: 20))}))
                .padding()
        }
    }
swift xcode modal-dialog textfield swiftui
7个回答
32
投票

iOS 15

有一个名为

@FocusState
的新包装器,它控制键盘和聚焦键盘(又名“firstResponder”)的状态。

⚠️注意,如果你想让它在初始时间集中,你必须应用延迟。这是 SwiftUI 的一个已知错误。

成为第一响应者(专注)

如果在文本字段上使用

focused
修饰符,则可以使它们成为焦点,例如,您可以在代码中设置
focusedField
属性来使绑定的文本字段变为活动状态:

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

或通过将变量设置为

nil
来关闭键盘:

不要忘记观看 WWDC2021 的 SwiftUI 中的直接和反映焦点会议


iOS 13 和 14(和 15)

老但工作:

简单的包装结构 - 像原生一样工作:

请注意根据评论中的要求添加了文本绑定支持

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
}

23
投票

由于 Responder Chain 不提供通过 SwiftUI 来使用,因此我们必须使用 UIViewRepresentable 来使用它。 我已经制定了一种解决方法,其工作方式与我们使用 UIKit 的方式类似。

 struct CustomTextField: UIViewRepresentable {

   class Coordinator: NSObject, UITextFieldDelegate {

      @Binding var text: String
      @Binding var nextResponder : Bool?
      @Binding var isResponder : Bool?

      init(text: Binding<String>,nextResponder : Binding<Bool?> , isResponder : Binding<Bool?>) {
        _text = text
        _isResponder = isResponder
        _nextResponder = nextResponder
      }

      func textFieldDidChangeSelection(_ textField: UITextField) {
        text = textField.text ?? ""
      }
    
      func textFieldDidBeginEditing(_ textField: UITextField) {
         DispatchQueue.main.async {
             self.isResponder = true
         }
      }
    
      func textFieldDidEndEditing(_ textField: UITextField) {
         DispatchQueue.main.async {
             self.isResponder = false
             if self.nextResponder != nil {
                 self.nextResponder = true
             }
         }
      }
  }

  @Binding var text: String
  @Binding var nextResponder : Bool?
  @Binding var isResponder : Bool?

  var isSecured : Bool = false
  var keyboard : UIKeyboardType

  func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
      let textField = UITextField(frame: .zero)
      textField.isSecureTextEntry = isSecured
      textField.autocapitalizationType = .none
      textField.autocorrectionType = .no
      textField.keyboardType = keyboard
      textField.delegate = context.coordinator
      return textField
  }

  func makeCoordinator() -> CustomTextField.Coordinator {
      return Coordinator(text: $text, nextResponder: $nextResponder, isResponder: $isResponder)
  }

  func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
       uiView.text = text
       if isResponder ?? false {
           uiView.becomeFirstResponder()
       }
  }

}

你可以像这样使用这个组件...

struct ContentView : View {

@State private var username =  ""
@State private var password =  ""

// set true , if you want to focus it initially, and set false if you want to focus it by tapping on it.
@State private var isUsernameFirstResponder : Bool? = true
@State private var isPasswordFirstResponder : Bool? =  false


  var body : some View {
    VStack(alignment: .center) {
        
        CustomTextField(text: $username,
                        nextResponder: $isPasswordFirstResponder,
                        isResponder: $isUsernameFirstResponder,
                        isSecured: false,
                        keyboard: .default)
        
        // assigning the next responder to nil , as this will be last textfield on the view.
        CustomTextField(text: $password,
                        nextResponder: .constant(nil),
                        isResponder: $isPasswordFirstResponder,
                        isSecured: true,
                        keyboard: .default)
    }
    .padding(.horizontal, 50)
  }
}

这里的isResponder是给当前textfield分配响应者,nextResponder是做第一个响应,因为当前textfield放弃了它。


9
投票

SwiftUIX 解决方案

使用

SwiftUIX
非常简单,我很惊讶更多的人不知道这一点。

  1. 通过 Swift Package Manager 安装 SwiftUIX。
  2. 在您的代码中,
    import SwiftUIX
  3. 现在您可以使用
    CocoaTextField
    代替
    TextField
    来使用功能
    .isFirstResponder(true)
CocoaTextField("Confirmation Code", text: $confirmationCode)
    .isFirstResponder(true)

4
投票

我认为 SwiftUIX 有很多方便的东西,但这仍然是你控制范围之外的代码,谁知道当 SwiftUI 3.0 出现时,糖魔法会发生什么。 请允许我介绍一下无聊的 UIKit 解决方案,经过合理的检查和升级的时间安排,略有升级

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)

// AutoFocusTextField.swift

struct AutoFocusTextField: UIViewRepresentable {
    private let placeholder: String
    @Binding private var text: String
    private let onEditingChanged: ((_ focused: Bool) -> Void)?
    private let onCommit: (() -> Void)?
    
    init(_ placeholder: String, text: Binding<String>, onEditingChanged: ((_ focused: Bool) -> Void)? = nil, onCommit: (() -> Void)? = nil) {
        self.placeholder = placeholder
        _text = text
        self.onEditingChanged = onEditingChanged
        self.onCommit = onCommit
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
        let textField = UITextField()
        textField.delegate = context.coordinator
        textField.placeholder = placeholder
        return textField
    }
    
    func updateUIView(_ uiView: UITextField, context:
                        UIViewRepresentableContext<AutoFocusTextField>) {
        uiView.text = text
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // needed for modal view to show completely before aufo-focus to avoid crashes
            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 ?? ""
        }
        
        func textFieldDidEndEditing(_ textField: UITextField) {
            parent.onEditingChanged?(false)
        }
        
        func textFieldDidBeginEditing(_ textField: UITextField) {
            parent.onEditingChanged?(true)
        }
        
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            parent.onCommit?()
            return true
        }
    }
}

 //   SearchBarView.swift



struct SearchBarView: View {
    @Binding private var searchText: String
    @State private var showCancelButton = false
    private var shouldShowOwnCancelButton = true
    private let onEditingChanged: ((Bool) -> Void)?
    private let onCommit: (() -> Void)?
    @Binding private var shouldAutoFocus: Bool
    
    init(searchText: Binding<String>,
         shouldShowOwnCancelButton: Bool = true,
         shouldAutofocus: Binding<Bool> = .constant(false),
         onEditingChanged: ((Bool) -> Void)? = nil,
         onCommit: (() -> Void)? = nil) {
        _searchText = searchText
        self.shouldShowOwnCancelButton = shouldShowOwnCancelButton
        self.onEditingChanged = onEditingChanged
        _shouldAutoFocus = shouldAutofocus
        self.onCommit = onCommit
    }
    
    var body: some View {
        HStack {
            HStack(spacing: 6) {
                Image(systemName: "magnifyingglass")
                    .foregroundColor(.gray500)
                    .font(Font.subHeadline)
                    .opacity(1)
                
                if shouldAutoFocus {
                    AutoFocusTextField("Search", text: $searchText) { focused in
                        self.onEditingChanged?(focused)
                        self.showCancelButton.toggle()
                    }
                    .foregroundColor(.gray600)
                    .font(Font.body)
                } else {
                    TextField("Search", text: $searchText, onEditingChanged: { focused in
                        self.onEditingChanged?(focused)
                        self.showCancelButton.toggle()
                    }, onCommit: {
                        print("onCommit")
                    }).foregroundColor(.gray600)
                    .font(Font.body)
                }
                
                Button(action: {
                    self.searchText = ""
                }) {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.gray500)
                        .opacity(searchText == "" ? 0 : 1)
                }.padding(4)
            }.padding([.leading, .trailing], 8)
            .frame(height: 36)
            .background(Color.gray300.opacity(0.6))
            .cornerRadius(5)
            
            if shouldShowOwnCancelButton && showCancelButton  {
                Button("Cancel") {
                    UIApplication.shared.endEditing(true) // this must be placed before the other commands here
                    self.searchText = ""
                    self.showCancelButton = false
                }
                .foregroundColor(Color(.systemBlue))
            }
        }
    }
}

#if DEBUG
struct SearchBarView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SearchBarView(searchText: .constant("Art"))
                .environment(\.colorScheme, .light)
            
            SearchBarView(searchText: .constant("Test"))
                .environment(\.colorScheme, .dark)
        }
    }
}
#endif

// MARK: Helpers

extension UIApplication {
    func endEditing(_ force: Bool) {
        self.windows
            .filter{$0.isKeyWindow}
            .first?
            .endEditing(force)
    }
}

// ContentView.swift

class SearchVM: ObservableObject {
    @Published var searchQuery: String = ""
  ...
}

struct ContentView: View {
  @State private var shouldAutofocus = true
  @StateObject private var viewModel = SearchVM()
  
   var body: some View {
      VStack {
          SearchBarView(searchText: $query, shouldShowOwnCancelButton: false, shouldAutofocus: $shouldAutofocus)
      }
   }
}

4
投票

我试图根据之前的答案使其变得简单,这使得视图出现时键盘出现,没有其他。 刚刚在iOS 16上测试,确实自动出现,无需设置延迟。

struct MyView: View {
    @State private var answer = ""
    @FocusState private var focused: Bool // 1. create a @FocusState here
    
    var body: some View {
        VStack {
            TextField("", text: $answer)
                .focused($focused) // 2. set the binding here
        }
        .onAppear {
            focused = true // 3. pop the keyboard on appear
        }
    }
}

1
投票

对于 macOS 13,有一个不需要延迟的新修改器。目前,不适用于 iOS 16。

VStack {
    TextField(...)
        .focused($focusedField, equals: .firstField)
    TextField(...)
        .focused($focusedField, equals: .secondField)
}.defaultFocus($focusedField, .secondField) // <== Here

Apple 文档:defaultFocus()


0
投票

要做的一件事是,重点关注支持的操作系统版本。

@available(iOS 15.0, *)
struct focusTextField: ViewModifier {
    @FocusState var textFieldFocused: Bool

    init(focused: Bool) {
        self.textFieldFocused = focused
    }
    func body(content: Content) -> some View {
        content
            .focused($textFieldFocused)
            .onTapGesture {
                textFieldFocused = true
            }
    }
}

struct nonfocusTextField: ViewModifier {
    func body(content: Content) -> some View {
        content
    }
}

extension View {
    func addFocus(textFieldFocused: Bool) -> some View {
        if #available(iOS 15.0, *) {
            return modifier(focusTextField(focused: textFieldFocused))
        } else {
            return modifier(nonfocusTextField())
        }
    }
}

这可以像

一样使用
TextField("Name", text: $name)
    .addFocus(textFieldFocused: nameFocused)

这可能并不适合所有情况,但非常适合我的用例

© www.soinside.com 2019 - 2024. All rights reserved.