Autofocus TextField programmatically in SwiftUI
Asked Answered
J

7

46

I'm using a modal to add names to a list. When the modal is shown, I want to focus the TextField automatically, like this:

Preview

I've not found any suitable solutions yet.

Is there anything implemented into SwiftUI already in order to do this?

Thanks for your help.

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()
        }
    }
Judejudea answered 9/10, 2019 at 19:29 Comment(5)
It's not currently possible since there is no responder chain support. You can wrap a real UITextField in a UIViewRepresentable and achieve what you want, but it will be more work.Cultigen
Possible duplicate of How to make TextField become first responder?Mcdermott
Why to press add user, and then to press add again ? Sure it's only to demonstrate the problem.Miyasawa
@Cultigen Keyboard handling and focus seem to be a big missing part with SwiftUI, so hopefully we'll see them in upcoming betas. Definitely an issue if we don't seem them soon.Cryptography
@Cultigen is there a way to allow the keyboard to 'tab' the the next textfield when a form has multiple text fields?Onomastics
W
36

iOS 15

There is a new wrapper called @FocusState that controls the state of the keyboard and the focused keyboard ('aka' firstResponder).

⚠️ Note that if you want to make it focused at the initial time, you MUST apply a delay. It's a known bug of the SwiftUI.

Become First Responder ( Focused )

If you use a focused modifier on the text fields, you can make them become focused, for example, you can set the focusedField property in the code to make the binded textField become active:

demo

Resign first responder ( Dismiss keyboard )

or dismiss the keyboard by setting the variable to nil:

enter image description here

Don't forget to watch the Direct and reflect focus in SwiftUI session from WWDC2021


iOS 13 and 14 (and 15)

Old but working:

Simple wrapper struct - Works like a native:

Note that Text binding support added as requested in the comments

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

Usage:

struct ContentView: View {
    @State var text = ""
    @State var isFirstResponder = false

    var body: some View {
        LegacyTextField(text: $text, isFirstResponder: $isFirstResponder)
    }
}

🎁 Bonus: Completely customizable

LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
    $0.textColor = .red
    $0.tintColor = .blue
}
Watson answered 8/6, 2021 at 17:45 Comment(7)
This doesn't actually answer how to make it happen automaticallyExpeditionary
"If you use a focused modifier on the text fields, you can make them become focused" This how. what are you looking for?Watson
@MojtabaHosseini I think your updateUIView needs to actually make use of the custom configuration with something like: self.configuration(uiView)Tracitracie
The question asks how to move focus to a TextField when presenting a sheet, not when tapping a button. How do you set the initial value of focusedField when presenting a sheet, ie, when using sheet(isPresented:onDismiss:content:)? @SamSoffes did you figure it out?Gnathonic
Just use the code in onAppear modifier. @GnathonicWatson
I tried that. It didn't do anything. And by adding a print statement in onAppear, I confirmed that onAppear gets called. It gets called when I press the button that presents the sheet, before the sheet is finished being presented. In onAppear, I also tried changing the value of the state property bound to the TextField, but that change doesn't happen until the focus moves to the TextField after I tap it. Also, I only want to move focus to the TextField when the sheet is presented, not every time the sheet appears.Gnathonic
@Gnathonic I was facing the same problem and a delay of 0.6 in onAppear worked for me. It seems to be a problem of the modal view, before adding the modal i was presenting the view in a ZStack and i was using a delay of 0.01 only. I think the delay is needed in all cases.Transeunt
F
23

Since Responder Chain is not presented to be consumed via SwiftUI, so we have to consume it using UIViewRepresentable. I have made a workaround that can work similarly to the way we use to do using 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()
       }
  }

}

You can use this component like this...

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

Here isResponder is to assigning responder to the current textfield, and nextResponder is to make the first response , as the current textfield resigns it.

Favouritism answered 9/4, 2020 at 12:30 Comment(3)
Would you know why passing nil to nextResponder ends up with error: "'nil' is not compatible with expected argument type 'Binding<Bool?>'"? If it's an optional value, doesn't mean, I can pass in a nil?Hoenack
@Hoenack you cannot pass a nil value directly to it, as it is a Binding variable, so you would need a constant state object with a nil value to make it work. try using '.constant(nil)' instead of simple nil. This will remove the error.Favouritism
This is not an answer to the question, because SwiftUI is cross-platform but UIViewRepresentable isn't. So for example, I'm designing a native Mac app (not Mac Catalyst) so I cannot use this answer. There needs to be a way to do this in SwiftUI...Sievers
H
9

SwiftUIX Solution

It's super easy with SwiftUIX and I am surprised more people are not aware about this.

  1. Install SwiftUIX through Swift Package Manager.
  2. In your code, import SwiftUIX.
  3. Now you can use CocoaTextField instead of TextField to use the function .isFirstResponder(true).
CocoaTextField("Confirmation Code", text: $confirmationCode)
    .isFirstResponder(true)
Hoenack answered 26/10, 2020 at 22:55 Comment(2)
This solution doesn't play well with NavigationView transitions (i.e. when transitioning from a view to another).Casey
How do you use responder chains with SwiftUIX's CocoaTextField?Molecule
S
8

I tried to make it simple based on previous answers, this makes the keyboard appear when view appears, nothing else. Just tested on iOS 16, it does appear automatically without the need to set a delay.

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
        }
    }
}
Sodomite answered 16/4, 2023 at 23:6 Comment(0)
C
4

I think SwiftUIX has many handy stuff, but that is still the code outside of your control area and who knows what happens to that sugar magic when SwiftUI 3.0 comes out. Allow me to present the boring UIKit solution slightly upgraded with reasonable checks and upgraded timing 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)
      }
   }
}
Creatural answered 30/12, 2020 at 20:29 Comment(0)
B
2

One thing to do is, make focus available on supported OS versions.

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

and this can be used like

TextField("Name", text: $name)
    .addFocus(textFieldFocused: nameFocused)

this might not be perfect for all cases, but works well for my use case

Brandtr answered 17/9, 2023 at 15:13 Comment(0)
Y
1

For macOS 13, there is a new modifier that does not require a delay. Currently, does not work on iOS 16.

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

Apple Documentation: defaultFocus()

Yasui answered 24/9, 2022 at 7:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.