Focus change in TextField and SecureField
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:
- 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.
- Create the underlying
UITextField
instance. - Respond to changes in bindings and update the underlying
UITextField
. - Use a
Coordinator
as aUITextFieldDelegate
to change theisFocused
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)
}
}