Sign in with GitHub to watch this episode for free.

Combine Essentials

Observing a Text Field with Combine

What I love most about RxSwift is RxCocoa. RxCocoa defines a wide range of integrations with UIKit and AppKit. It is surprising that Apple's Combine framework lacks these integrations. The good news is that most of the integrations RxCocoa defines are easy to implement using Combine. In this episode, I show you how to observe the value of a UITextField instance using the Combine framework.

RxSwift and RxCocoa

Before I explain you how to observe the value of a UITextField instance with Combine, I want to show you what I have in mind. This is what it looks like to observe the text property of a text field with RxSwift and RxCocoa.

usernameTextField.rx.text
    .subscribe(onNext: { text in
        print(text)
    }).disposed(by: disposeBag)

The rx namespace is convenient, but I would like to create an API that is similar to that of URLSession. Let me explain what I mean by that. To create a data task publisher, you simply invoke the dataTaskPublisher(for:) method on a URLSession instance, passing in a URL object or a URLRequest object.

let request = URLRequest(url: url)
URLSession.shared.dataTaskPublisher(for: request)
    .sink(receiveCompletion: { completion in
        print(completion)
    }, receiveValue: { data, response in
        print(response)
    }).store(in: &subscriptions)

The idea is to extend UITextField by defining a computed property, textPublisher, that returns a publisher with String? as its Output type and Never as its Failure type. This is what that looks like. That looks pretty nice. Right?

usernameTextField.textPublisher
    .sink(receiveValue: { text in
        print(text)
    }).store(in: &subscriptions)

Extending UITextField

We create an extension for UITextField and declare a computed property, textPublisher. As I mentioned earlier, the Output type of the publisher is String? because that is the type of the text property of UITextField. It shouldn't be possible for the publisher to complete or terminate with an error. That means the Failure type of the publisher is Never.

extension UITextField {

    var textPublisher: AnyPublisher<String?, Never> {
    	
    }

}

The implementation is fairly simple thanks to Foundation's support for the Combine framework. In the body of the computed property, we create a publisher that emits UITextField.textDidChangeNotification notifications for self, the UITextField instance. We ask the default notification center for such a publisher by invoking the publisher(for:object:) method, passing in UITextField.textDidChangeNotification as the first argument and self, the UITextField instance, as the second argument. By passing the text field as the second argument, the publisher only emits notifications if the text property of the text field changes.

extension UITextField {

    var textPublisher: AnyPublisher<String?, Never> {
        NotificationCenter.default.publisher(
            for: UITextField.textDidChangeNotification,
            object: self
        )
    }

}

The publisher emits notifications, but we need the value of the text property of the text field. We apply the map operator to the publisher, passing in a closure that accepts the notification as an argument. We use a guard statement to cast the object of the notification to UITextField. While this should never fail, it doesn't hurt to play it safe. The closure returns nil if the cast fails and returns the value of the text property if the object of the notification is a UITextField instance.

extension UITextField {

    var textPublisher: AnyPublisher<String?, Never> {
        NotificationCenter.default.publisher(
            for: UITextField.textDidChangeNotification,
            object: self
        )
        .map { notification in
            guard let textField = notification.object as? UITextField else {
                return nil
            }

            return textField.text
        }
    }

}

You can omit the guard statement and use shorthand argument names if you prefer a more concise implementation.

extension UITextField {

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

}

To keep the compiler happy, we wrap the publisher the map operator returns with a type eraser using the eraseToAnyPublisher() method.

extension UITextField {

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

}

You can replace the map operator with the compactMap operator if you want the Output type of textPublisher to be String.

extension UITextField {

    var textPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default.publisher(
            for: UITextField.textDidChangeNotification,
            object: self
        )
        .compactMap { ($0.object as? UITextField)?.text }
        .eraseToAnyPublisher()
    }

}

With this extension in place, you can ask any UITextField instance for a publisher that emits the value of its text property.

usernameTextField.textPublisher
    .sink(receiveValue: { text in
        print(text)
    }).store(in: &subscriptions)

What's Next?

The RxCocoa library uses similar techniques to integrate with UIKit and AppKit. I hope this episode shows that you can replicate what RxCocoa offers using Apple's Combine framework.