Gualtiero Frigerio
Posted on July 3, 2019
Original article at http://www.gfrigerio.com/combine-first-example/
This is a post about Combine, the new framework for FRP from Apple.
There are two great WWDC sessions you can watch about it:
Introducing Combine
Combine in practice
And this is the official documentation
What I’m going to do is implementing Combine as show in the session Combine in practice to enable or disable a button based on the values of some UITextFields.
I think the guys on stage did a great job in explaining the framework, but having a GitHub project with actual code can help understanding their example, as they show snippets of code but not the entire project. That’s what this post is all about.
Combine
First let me write a really quick introduction to Combine and two main concepts: publishers and subscribers. As the names suggest, you have a Publisher that publish, or share, a sequence of values over time.
A Subscriber subscribes, or receive, the values from a publisher and can control the way it receives them.
A little bit of a background: a Publisher is a protocol with two associated types: an Output (String, Bool etc.) and the Failure, Never in our example, or an Error. It has a subscribe function, so a Subscriber, matching the two associated types, can subscribe to the publisher.
Subscriber is a protocol with two associated types, Input (must match the publisher’s Output) and Failure.
Publishers can be chained, or combined together to produce another publisher, with a different type (we need what Combine calls an Operator, and you’ll see we can use .map for that) and we’ll use AnyCancellable to avoid retain cycles. If you’re familiar with RxSwift that’s the DisposeBag concept.
Sample project
You can find the example code on GitHub here
We have 3 UITextField connected to a view controller, and a UIButton that we want to enable only when the username is valid (> 3 characters) and the password match and are valid (again > 3 characters).
Let’s suppose we want to achieve the same goal without Combine. We could have a function to enable the button, and we could call that function every time one of the 3 UITextField changes. But what if we need to call a web service to find out if the username is valid or exists? We should wait for the web service to respond, then again check the values of the two password fields and enable or disable the button. Things can get complicated, and you need to manually call something every time one of the UITextField changes. Not a good idea…
Ok so we want to use Combine, let’s do it!
First I’ll show you the implementation of the ViewController. I decided to use a standard UIViewController and avoid SwiftUI, so we can focus on Combine and apply it to something we’re familiar with.
private var viewModel = RegistrationViewModel()
@IBAction func usernameDidChange(_ sender: UITextField) {
viewModel.username = sender.text ?? ""
}
@IBAction func passwordDidChange(_ sender: UITextField) {
viewModel.password = sender.text ?? ""
}
@IBAction func passwordRepeatDidChange(_ sender: UITextField) {
viewModel.passwordRepeat = sender.text ?? ""
}
So as you can see every time one of the UITextField changes we update the variables in our view model. I preferred to keep the Combine logic in a different class (a struct actually) so the ViewController only deals with the UI. We’re not directly calling a function to perform the validation every time one of the text fields changes, Combine will deal with that.
class RegistrationViewModel {
@Published var username:String = ""
@Published var password:String = ""
@Published var passwordRepeat:String = ""
}
Wait a minute, what is @Published before the keyword var? It is something new in Swift 5.1, it is called property wrapper (aka property delegate) and allows the compiler to automatically wrap a property with a specific type. In our example @Published adds a publisher to a property, so username has a publisher. When you use property wrappers the new wrapped variable can be accessed by using $, so in case of username you can access the publisher via $username. Property wrappers are used a lot in SwiftUI and I love the concept so I’ll write about them in future articles.
Ok, now you now what @Published means, we have 3 publishers, each of them will publish a new value every time username, password and passwordRepeat are set via the IBAction we saw before.
Let’s merge them together, using some more variables, starting with the password. We want to check that both fields are equal, and we want the password to be at least 4 characters.
var validPassword:AnyPublisher {
return $password.combineLatest($passwordRepeat) { (password, passwordRepeat) in
var isValid = false
isValid = (password == passwordRepeat) && password.count > 3
return isValid
}.eraseToAnyPublisher()
}
What is AnyPublisher? It is a type of Publisher that can be cancelled (we get this behaviour via eraseToAnyPublisher), as we said before this is necessary for memory management. I hope you’re familiar with generics, here we say that this AnyPublisher is going to publish a Bool, and produce an error of type Never (so it doesn’t fail).
As I mentioned before we use $password to access the wrapped version of password, which has a Publisher. Then we use combineLatest, to combine together this publisher and a second one, in this case $passwordRepeat so the second password field. In the closure we have the values of the two fields, and we return a Bool. Then the modifier .eraseToAnyPublisher returns an AnyPublisher, instead of a Publisher.
Let’s take a look at the username publisher now. In theory, we could combine it with validPassword, but I wanted to implement the same modifier they talked about in the session Combine in practice.
var validUsername:AnyPublisher {
return $username
.debounce(for: 0.2, scheduler: RunLoop.main)
.removeDuplicates()
.map{$0.count > 3 ? true : false}
.eraseToAnyPublisher()
}
debounce avoid publishing changes as they happen, but introduces a delay. This way if the user is typing really fast we don’t continue to publish the value and check if the username is valid. In the example the check is very simple, but if you need to call a REST API you don’t want to make calls every time a new character is typed, you better wait for a little bit. .removeDuplicates deal, guess what, with duplicates. If you type username then usernam then username again it doesn’t make sense to check for the same value twice, so the publisher won’t publish the same value over and over again.
$username is a publisher of type String, as the value is a String, so we need .map to convert it to a Bool as the publisher we want to expose it . And once again .eraseToAnyPublisher makes the AnyPublisher that is cancellable.
We’re getting close to the end, but let’s see another way to combine two publishers together.
var validCredential:AnyPublisher {
return Publishers.CombineLatest(validUsername, validPassword) { (validUsername, validPassword) in
return validUsername && validPassword
}.eraseToAnyPublisher()
}
CombineLatests returns a new publisher combining the value of two publishers, is similar to the one we saw before for password but I find it easier to read, so I’d prefer this implementation over the other one. This is the one they talked about on stage if you watch the video. This time we don’t need map as both publishers publish a Bool and we want an AnyPublisher of type Bool.
We’re almost done, but we need to get back to our ViewController. The ViewModel is exposing validCredential, and the ViewController can subscribe to it so every time a new value is published, either true or false, the button can be enabled or disabled.
private var registerButtonSubscriber:AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
registerButtonSubscriber = viewModel.validCredential
.receive(on: RunLoop.main)
.assign(to: \.isEnabled, on: registerButton)
}
Let me start by describing the registerButtonSubscriber var, of type AnyCancellable. we use it so we can cancel the subscription on deinit() and we avoid retain cycles.
And now the one line of code I’m sure you were craving for, we can finally subscribe to the validCredential publisher, receive its update on the main thread (this is important as we deal with UI) and assign the published value to the isEnabled property of registerButton.
If you try the sample project you’ll see the button is activated and disabled every time you type something on one of the 3 UITextFields. Pretty cool, isn’t it?
Of course there is way more than that, I haven’t talked about other publishers like PassthroughSubject and I think Combine would be great in my past project I used to write about RxSwift and Promises, so stay tuned for more!
Posted on July 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.