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:

preview

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

Previous Post