Reading time: 2 min

Detecting if a text field gained or lost focus is not entirely straightforward in SwiftUI.

TextField constructor has a parameter named onEditingChanged, but it triggers only when the user taps the return key in the virtual keyboard, not when the focus is actually lost.

Even worse, SecureField doesn't have such a parameter, and there's no way to know if the user is interacting with the view!

As usual, the solution is to wrap a UITextField via UIViewRepresentable, and expose bindings to the required state changes:

struct MyTextField: UIViewRepresentable {

    // 1
    @Binding var text: String
    @Binding var isRevealed: Bool
    @Binding var isFocused: Bool

     // 2
    func makeUIView(context: UIViewRepresentableContext<MyTextField>) -> UITextField {
        let tf = UITextField(frame: .zero)
        tf.isUserInteractionEnabled = true
        tf.delegate = context.coordinator
        return tf
    }

    func makeCoordinator() -> MyTextField.Coordinator {
        return Coordinator(text: $text, isFocused: $isFocused)
    }

    // 3
    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.text = text
        uiView.isSecureTextEntry = !isRevealed
    }

    // 4
    class Coordinator: NSObject, UITextFieldDelegate {
        @Binding var text: String
        @Binding var isFocused: Bool

        init(text: Binding<String>, isEnabled: Binding<Bool>, isFocused: Binding<Bool>) {
            _text = text
            _isFocused = isFocused
        }

        func textFieldDidChangeSelection(_ textField: UITextField) {
            text = textField.text ?? ""
        }

        func textFieldDidBeginEditing(_ textField: UITextField) {
            DispatchQueue.main.async {
               self.isFocused = true
            }
        }

        func textFieldDidEndEditing(_ textField: UITextField) {
            DispatchQueue.main.async {
                self.isFocused = false
            }
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            textField.resignFirstResponder()
            return false
        }
    }
}

What happens here is:

  1. You declare the necessary bindings:
    • One for text content.
    • One for determining if the field is secure or not.
    • One that tracks if the field is focused or not.
  2. Create the underlying UITextField instance.
  3. Respond to changes in bindings and update the underlying UITextField.
  4. Use a Coordinator as a UITextFieldDelegate to change the isFocused binding when, well, the focus changes.

The isRevealed binding allows you to trivially discern between a normal and secure text field, as well as to implement a "Show/Hide" behavior for password fields.

Here's a sample password text field, with a show/hide button, that changes appearance if focused:

struct MyPasswordField: View {
    @Binding var text: String
    @State private var isRevealed = false
    @State private var isFocused = false

    var body: some View {
        HStack {
            MyTextField(text: $text,
                        isRevealed: $isRevealed,
                        isFocused: $isFocused)
            Button(action: {
                self.isRevealed.toggle()
            }) {
                Image(systemName: self.isRevealed ? "eye.slash.fill" : "eye.fill")
                    .foregroundColor(.themeBlue)
            }
        }
            .foregroundColor(.textBlack)
            .padding()
            .border(isFocused ? Color.blue : Color.gray)
            .background(isFocused ? Color.white : Color.gray)
            .frame(height: 50)
    }
}

Next Post Previous Post