Swift Combine - How to get a Publisher that delivers events for every character change of UITextField's text property

Viewed 6867

I noticed that

textField.publisher(for: \.text)

delivers events when editing finishes, but not for every character/editing change. How do I get a Publisher, that sends evens for every change? In ReactiveSwift it would be

textField.reactive.continousTextValues()

And in RxSwift it would be just (How do you get a signal every time a UITextField text property changes in RxSwift)

textField.rx.text

Approaches I took:

  • checking the publisher(for:options:) method, but there are no appropriate options for the desired outcome.
  • adding a target/action textField.addTarget(self, action: #selector(theTextFieldDidChange), for: .editingChanged) (UITextField text change event)
  • doing essentially the same as the previous step by connecting the action via interface builder, both leading to extra work and cluttered code.
  • Watching the 2019 WWDC videos about Combine. They were not dealing with textfields, but using @Published variables instead, hiding were the values actually came from - (or did I miss something?).

I have no clue at the moment, how to do this and I feel the tendency to go back to ReactiveSwift, but I wanted to ask you, before taking this step backwards.

4 Answers

In case of a UITextField you can use this extension:

  extension UITextField {
    func textPublisher() -> AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: self)
            .map { ($0.object as? UITextField)?.text  ?? "" }
            .eraseToAnyPublisher()
    }
  }

use as: textField.textPublisher()

Unfortunately, Apple didn't add publishers for this to UIKit and because UIControl doesn't implement KVO the publisher(for:) publisher doesn't work quite as expected. If you're okay with adding a dependency to your project there are a couple of good UIControl publishers here.

Alternatively, you could implement your own publisher that does this, or subscribe to textfield changes the old school way.

If this is what you are looking for, I guess UITextField emits both notifications and controlevents, so you can achieve that by listening to notifications for example:

import UIKit
import Combine

class ViewController: UIViewController {

    @IBOutlet private var textField: UITextField!

    var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        let textFieldPublisher = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .map( {
                ($0.object as? UITextField)?.text
            })
        
        textFieldPublisher
            .receive(on: RunLoop.main)
            .sink(receiveValue: { [weak self] value in
                print("UITextField.text changed to: \(value)")
            })
            .store(in: &cancellables)
    }
}

Let me know if this was your actual goal.

Related