10
Jan
2023
Detect Backspace in SwiftUI
Reading time: 2 min
This recipe shows how to detect backspace event in SwiftUI TextField. The method can figure out both if it's a forward or in-text backspace, and doesn't rely on hacks on hidden characters.
The end result looks like this:
There are numerous solutions out there that try to figure out if backspace was pressed by introducing hidden characters (such as zero-width space), but those are hacky and don't work well as they:
- Mess up the cursor and selection,
- Mess up autocompletion,
- Hide the placeholder (since the textfield technically isn't empty anymore), and
- Force additional input processing to eliminate the extra hidden character(s).
The solution in this recipe creates a custom UITextField
subclass and overrides its deleteBackward
method. Then, it wraps this custom view and exposes it to SwiftUI using UIViewRepresentable
.
Here's the code for the custom UITextField
and its SwiftUI wrapper:
struct EnhancedTextField: UIViewRepresentable {
let placeholder: String // text field placeholder
@Binding var text: String // input binding
let onBackspace: (Bool) -> Void // true if backspace on empty input
func makeCoordinator() -> EnhancedTextFieldCoordinator {
EnhancedTextFieldCoordinator(textBinding: $text)
}
func makeUIView(context: Context) -> EnhancedUITextField {
let view = EnhancedUITextField()
view.placeholder = placeholder
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: EnhancedUITextField, context: Context) {
uiView.text = text
uiView.onBackspace = onBackspace
}
// custom UITextField subclass that detects backspace events
class EnhancedUITextField: UITextField {
var onBackspace: ((Bool) -> Void)?
override init(frame: CGRect) {
onBackspace = nil
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError()
}
override func deleteBackward() {
onBackspace?(text?.isEmpty == true)
super.deleteBackward()
}
}
}
// the coordinator is here to allow for mapping text to the
// binding using the delegate methods
class EnhancedTextFieldCoordinator: NSObject {
let textBinding: Binding<String>
init(textBinding: Binding<String>) {
self.textBinding = textBinding
}
}
extension EnhancedTextFieldCoordinator: UITextFieldDelegate {
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
textBinding.wrappedValue = textField.text ?? ""
return true
}
}
Then, you can put it to use like this:
struct BackspaceEventTest: View {
@State private var inputText = ""
var body: some View {
EnhancedTextField(placeholder: "Input text", text: $inputText) { onEmpty in
print("Backspace pressed, onEmpty? \(onEmpty) at \(Date().ISO8601Format())")
}
}
}