40

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()
        }
    }
Nils
  • 1,755
  • 3
  • 13
  • 25
  • 1
    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. – Procrastin8 Oct 09 '19 at 19:34
  • 1
    Possible duplicate of [How to make TextField become first responder?](https://stackoverflow.com/questions/56507839/how-to-make-textfield-become-first-responder) – superpuccio Oct 09 '19 at 19:48
  • Why to press add user, and then to press add again ? Sure it's only to demonstrate the problem. – Marc T. Oct 10 '19 at 05:35
  • 2
    @Procrastin8 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. – Akbar Khan Oct 14 '19 at 05:17
  • 1
    @Procrastin8 is there a way to allow the keyboard to 'tab' the the next textfield when a form has multiple text fields? – Learn2Code Mar 17 '20 at 22:11

6 Answers6

32

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
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 6
    This doesn't actually answer how to make it happen automatically – Sam Soffes Aug 26 '21 at 04:53
  • "If you use a focused modifier on the text fields, you can make them become focused" This how. what are you looking for? – Mojtaba Hosseini Aug 26 '21 at 05:46
  • @MojtabaHosseini I think your `updateUIView` needs to actually make use of the custom `configuration` with something like: `self.configuration(uiView)` – nylki Sep 14 '21 at 16:30
  • 1
    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:)`](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:))? @SamSoffes did you figure it out? – ma11hew28 Nov 18 '21 at 04:25
  • Just use the code in `onAppear` modifier. @ma11hew28 – Mojtaba Hosseini Nov 18 '21 at 05:39
  • 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. – ma11hew28 Nov 18 '21 at 14:13
  • 3
    @ma11hew28 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. – Cristi Băluță Dec 11 '21 at 08:52
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.

fluktuid
  • 155
  • 8
Anshuman Singh
  • 1,018
  • 15
  • 17
  • Would you know why passing nil to nextResponder ends up with error: "'nil' is not compatible with expected argument type 'Binding'"? If it's an optional value, doesn't mean, I can pass in a nil? – Zorayr Jun 04 '20 at 20:14
  • 2
    @Zorayr 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. – Anshuman Singh Jun 05 '20 at 14:22
  • 7
    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... – scaly Aug 13 '20 at 03:56
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)
Zorayr
  • 23,770
  • 8
  • 136
  • 129
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)
      }
   }
}
Vivienne Fosh
  • 1,751
  • 17
  • 24
4

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
        }
    }
}
Skoua
  • 3,373
  • 3
  • 38
  • 51
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()

Mykel
  • 1,355
  • 15
  • 25