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.