MVVM is the critical design pattern for front-end engineers. There are so many ways that objects can talk to each other in an iOS App: delegates, callbacks, notification. In this Swift Language User Group talk, Max Alexander shows you how to streamline your development process in 3 easy patterns with RxSwift. He’ll go over the MVVM basics, creating custom observers, wrangling disparate APIs, and manipulating calls using concurrency and dispatch queues. Your code will be so neat that you could quit your job and leave it for the next guy in impeccable condition.
Introduction (00:00)
I am Max, and I am going to talk to you about MVVM with RxSwift. The first part of my talk is going to be about MVVM, and we will get to some code.
Massive View Controller (00:51)
MVVM solves the MVC pattern (everyone jokes about the “Massive View Controller”). It seems to be the case that many engineers start with a view controller (it feels good: it’s already bootstrapped for you when you start a new project). You shove everything in there. And you don’t stop. You don’t stop for years, and then you end up with legacy apps.
It’s hard to bring in new engineers because everyone is confused as to what functions are calling what, where the data is supposed to be, where the service classes are supposed to be, and where the UI is even supposed to be.
Instead of shoving everything into your view controller, we’re going to do a Model, View, and ViewModel.
View (01:37)
- UIButton
- UITextField
- UITableView
- UICollectionView
- NSLayoutConstraints
This is a way to split things up within your view controller, and then two different classes. The view is everything that’s normal, probably Interface Builder for you. You probably shove in UITextFields
, UITableViews
, you make constraints around them. Everyone who’s ever touched iOS or Mac development with the respective classes has done this quite often. If you have a UIViewController
, it looks something like this. This is how you start.
class LoginViewController : UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton
}
Then you have the model.
Model (02:10)
“Model” is an abstract term. It’s like data coming from disparate classes.
- CLLocationManager
- Alamofire
- Facebook SDK
- Database like Realm
- Service Classes
They could be Facebook API with Twitter API, different shapes, different formats; databases like Realm; and generic service classes. Many people say it’s a POJO (“Plain Old Java Object”) or a POCO (“Plain Old Class Object”). But it doesn’t matter: it means there’s data coming from somewhere. And if you have model classes, you probably will not find import UIKit
.
ViewModel (02:55)
The ViewModel:
- Prepares data
- Manipulates data
It’s sitting right between two things. If you have a login view controller, you have a username and a password member field. Then you have some function that you might call like Alamofire or Facebook API client. You can see that it’s preparing some data and then you can call it.
I did not show the full implementation, but you can understand where this is going. Now you have a LoginViewController
, and you set up your data members and then you have a LoginViewModel
.
struct LoginViewModel {
var username: String = ""
var password: String = ""
func attemptToLogin() {
let params = [
"username": username,
"password": password
]
ApiClient.shared.login(email: email, password: password) { (response, error) in
}
}
}
We have the UIViewController
and UIViews
(whatever you have that relates with visual presentation). It’s going to have member variables of subviews. You are going to create the struct (usually it’s a struct because it is data at a certain point of time; don’t use it as a class).
class LoginViewController : UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton
//viewModel is just a member variable here.
var viewModel = LoginViewModel()
override func viewDidLoad(){
super.viewDidLoad()
}
}
The viewModel
is a struct that has member variables, it has however you are going to talk with your service classes. The model is your API, you could have a wrapper around CLLocationManager
, contact store. These UI interactions can call functions within the ViewModel to get data through. In the last bit of code I probably showed you how the ViewModel exists and how you can call some of these other functions.
Hooking up the UI and the ViewModel is not simple because everyone is going to do it differently.
class LoginViewController : UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton
//viewModel is just a member variable here.
var viewModel = LoginViewModel()
override func viewDidLoad(){
super.viewDidLoad()
usernameTextField.addTarget(self, action:
#selector(LoginViewController.usernameTextFieldDidChange:_),
forControlEvents: UIControlEvents.EditingChanged)
passwordTextField.addTarget(self,
action: #selector(LoginViewController.passworldTextFieldDidChange:_),
forControlEvents: UIControlEvents.EditingChanged)
}
func usernameTextFieldDidChange(textField: UITextField){
viewModel.username = textField.text ?? ""
}
func passworldTextFieldDidChange(textField: UITextField){
viewModel.password = textField.text ?? ""
}
func confirmButtonTapped(sender: UIButton){
viewModel.attemptLogin()
}
}
You have your IBOutlet
s, classes with UIKit, ViewModel. In viewDidLoad
, you can use Interface Builder to jerry-rig your event handlers here. But then when the username text field changes, we’re going to mutate viewModel.username
with text. And then the password, the same thing. When a confirm button is tapped you can hook it up and call the attempt login on the ViewModel.
That was less fun because I missed one big part of it: UI that reacts to changing events. I showed you how the view can go to the ViewModel. But the view itself has to get updated: loading indicators, making sure the confirm button is disabled or enabled.
For example, form validation is going to the data and then back. This part is hard. This part where the ViewModel talks back to the UI is something everyone is going to do differently. Here’s one way to do it.
struct LoginViewModel {
var username: String = "" {
didSet {
evaluateValidity()
}
}
var password: String = "" {
didSet {
evaluateValidity()
}
}
var isValid : Bool = ""
func attemptToLogin() {
let params = [
"username": username,
"password": password
]
ApiClient.shared.login(email: email, password: password)
{ (response, error) in
}
}
private func evaluateValidity(){
isValid = username.characters.count > 0
&& password.characters.count > 0
}
}
People use didSet
as a reactionary handler. Whenever you use the UI to update username or password, you’re going to call and evaluate the validity of the form to make sure username
has some value in it, or password
; you can get as robust as you want.
You can mutate the isValid
form. But how do you bring this back to the form’s isDisabled
or isEnabled
state?
Make sure you never have the ViewModel file that you are editing ever have UIKit imported. It should never have a reference to UIViewController
. Because all it’s supposed to do is prepare data; it’s not supposed to mutate data at all. The ViewController owns the ViewModel. In the long run, if you have enough time and resources, also make these ViewModels testable.
Never ever say (see slides for code), “isValid
, I’m going to mutate that state of the confirm button to isDisabled
whenever that is didSet
. We created a weak LoginViewController and we’re going to jerry-rig it in the viewDidLoad
and set it; later when these two things fire the, evaluate validity, we are going to mutate this.”
This is not a good scenario. This means that you’re not getting any of the benefits of MVVM, because you’re saying, “I am going to shove references back and forth.” Plus you can get into weird situations where you will have a retain cycle because one object will be owning the other.
I have to make a side note because we have UI updating the username
and password
text fields, both of which are calling the same thing. You can see how many forms, especially very large forms like credit card forms, can get into the hundreds of lines by calling the same evaluation cycle.
We have two streams of data with didSet
s, calling evaluate validity. That’s going to call an isValid
which is going to call another mutation. We’re taking two different events, putting them into one, and then doing something with that data. This can be simplified vastly in the future. This is all synchronous code–what if we had this as asynchronous in the future as well?
View Model Worst Practices (09:06)
- Never reference the view controller
- Do not import UIKit. Make it a different file.
- Do not reference anything from UIKit. (You might think, “I need a button reference, I’m going to shove it in there.” Don’t do that.)
- It should only be data, i.e., strings, structs, JSON; classes that do not have much functionality.
Here’s a pattern I’ve seen. isValid
needs to now update the UI buttons’ state.
struct LoginViewModel {
var username: String = "" {
didSet {
evaluateValidity()
}
}
var password: String = "" {
didSet { evaluateValidity()
}
}
var isValid : Bool = "" {
didSet {
isValidCallback?(isValid: isValid)
}
}
var isValidCallback : ((isValid: Bool) -> Void)?
func attemptToLogin() {
//truncated for space
}
private func evaluateValidity(){
isValid = username.characters.count > 0
&& password.characters.count > 0
}
}
You have this closure, this little callback that you create.
class LoginViewController {
@IBOutlet var confirmButton: UIButton!
var loginViewModel = LoginViewModel()
override func viewDidLoad(){
super.viewDidLoad()
loginViewModel.isValidCallback = { [weak self] (isValid) in
self?.confirmButton.isEnabled = isValid
}
}
}
The ViewModel can listen to it. This is going to call this callback and return its value to some listener, and this listener is going to be the view controller. The LoginViewModel will have an isValid
callback and you will set that value. This is nice and testable because you can put it into the run-time and test for booleans instead of the entire state of the view controller.
That problem with unidirectional data flow, where the UI talks to the ViewModel and the ViewModel talks to the model, is a hard problem. You saw how easy this was, going from UIKit to ViewModel to Model, back to ViewModel.
This part, going from ViewModel back to UIKit, everyone is going to do differently. This is what RxSwift will solve for you.
RxSwift (11:38)
How can RxSwift help with this? RxSwift is Lodash for events, or Underscore for events, if you’re coming from the JavaScript world. It allows you to operate events, evented data, as if you were able to manipulate arrays or collections. Things changing over time is similar to something changing in an array.
I have a little playground, RxSwift (see video). An observable is a collection type, you can say. It’s going to emit events, some source that emits events, you can create observables that represent almost everything. An easy one to do is: you’re creating something like a stock ticker helper to tell you if you should buy something or not from a web socket. We’re going to fake it and create something.
You do observable float from an array, and these are stock prices that come up. 1, 2, 35, 90 are floats. In this playground, it has already run. You can subscribe to the source of events, and the source of events will keep on emitting and then you’re going to get the values back. The cool part of this is an easy one for example. And remember this can come in, pretend this is not literal, for example the stock prices is not updating every minute; it could be updating in varying amounts of time.
We want to tell the UI, to put UI code, for now, in the subscribe event. We want to buy when the price is higher than 30. We will print the prices as they come along. But we want to create another string from it.
The second string of data in this new observable we’re going to filter. And then it will only run this subscribe block if this filter works. RxSwift allows you to filter, to map them. For example, we could do something like this, map. We can do a exchange rate. For example, you’re trying to buy in a different country and you have an exchange rate. And then you’ll print out these new rates. We removed the filter, so it’s going to do it for every single different event.
There’s an easy subclass, almost like a substruct, of an observable. An observable is something that emits events and luckily with generics we always have types of them. There is a thing called a variable; you can always create them statically; they’re representations around very primitive types that you can use. And username and password are observables, because you can always get their values by username.value
, without subscribing to it.
This will be mutated from the UI, but we also want to listen to the isValid
state. We’ve created two streams, self.username
and self.password
, and we want to evaluate when any of them changes. That’s what we call the combineLatest
: it’s an operator, similar to Lodash operators like groupBy
.
If any of these change, we’ll run this reducer block, and it will return to us a boolean. If the username character count is greater than zero and password count is greater than zero, now this got rid of all the didSet
s, this got rid of any private methods, and created this stream of data. You go from the UI and the isValid
will return to the UI.
I typically write into my code fromUI
or toUI
. Or you could say fromViewModel
or toViewModel
. You still understand that you have two sections of code, that is one going this direction and one that is going the other direction.
The view controller will have your regular ViewModels but you have some extension methods within the RxCocoa library. They’re extension methods on top of UIKit,which allow you to get changes in values without creating delegates around them.
I hate delegates and I think they are the worst things in the entire world because they make you scroll. Especially with UITableView
and UICollectionView
. And simple things like having two different text views, you have to check the references from each one - it’s rather annoying. Here you can directly get the reference, on the ViewDidLoad, usernameTextField.rx.text
, and you are going to bindTo
the ViewModel. bindTo
is the same thing as subscribe for all intents and purposes.
You’ve probably used some APIs where you get indefinite events, but then you have to stop the token handler. A “dispose bag” is a collection of those. Whenever the dispose bag has deinitialized, it will dispose of any of these streams. It’s good for cleaning things up. And you can manually dispose of them as well by calling disposeBag.dispose
. This is going to the ViewModel and then the ViewModel now has the rx.isValid
. And we are going to map out the isEnabled
value.
I have a little example that does that. I truncated this for you; you probably noticed that rx.text
from the UITextField
gives you an optional string. I’m going to give it a default value of an empty string, to make things easier for you.
import RxSwift
struct LoginViewModel {
var username = Variable<String>("")
var password = Variable<String>("")
var isValid : Observable<Bool>{
return Observable.combineLatest( self.username, self.password)
{ (username, password) in
return username.characters.count > 0
&& password.characters.count > 0
}
}
}
We’re going to get a simple login form.
import RxSwift
import RxCocoa
class LoginViewController {
var usernameTextField: UITextField!
var passwordTextField: UITextField!
var confirmButton: UIBUtton!
var viewModel = LoginViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad(){
super.viewDidLoad()
usernameTextField.rx.text.bindTo(viewModel.username).addTo(disposeBag)
passwordTextField.rx.text.bindTo(viewModel.password).addTo(disposeBag)
//from the viewModel
viewModel.rx.isValid.map{ $0 }
.bindTo(confirmButton.rx.isEnabled)
}
}
As you can see, there is a login button, but it’s disabled. And then you are going to say password. All of a sudden login is now valid. That is a ton of code gone. If I remove it, it goes back.
If we put a breakpoint in LoginViewModel
, this will run if any of them changes; that’s what the combineLatest
allows you to do. It gets the state each time you get the values back. If you look over username, you’ll get its value and then password, you’ll get its value. And then it’ll return a boolean. This allows you to always mutate values as well as combine them to get new values out.
Make sure that the UIBindings do not talk to each other. I know many people were excited with RxCocoa because it got rid of many of their delegate talks. But make sure you do not have your UI streams talk to each other. It gets messy easily.
For example, you might decide, “I am not going to do this ViewModel thing.” And you create the combineLatest
by binding to your username text field and password text field. And then your result selector returns you this. And you create this isValid.bindTo
.
class ViewController : UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
let isValid Observable.combineLatest(
username.rx.text,
password.rx.text,
resultSelector: { (username, password) -> Bool in
return username.characters.count > 0
&& password.characters.count > 0
})
isValid.bindTo(confirmButton.rx.isEnabled)
.addTo(disposeBag)
}
}
This is completely untestable because isValid
is this ambient thing that stays in the view controller; you don’t get the reference to it, it just lives there. And you’re not getting any of the benefit of MVVM; it’s still a Massive View Controller.
RxCocoa does not cover the entire world of UI binding. It has lots of stuff that helps, like UITextField, UIButtonTaps, MapKit, and UITableView (you can create an entire career out of UITableView).
class MyCustomView : UIView {
var sink : AnyObserver<SomeComplexStructure> {
return AnyObserver { [weak self] event in
switch event {
case .next(let data):
self?.something.text = data.property.text
break
case .error(let error):
self?.backgroundColor = .red
case .completed:
self.alpha = 0
}
}
}
}
UITableView and UICollectionView are expensive because there are all these ways you want to do it, and getting rid of a row is a Stack Overflow lookup. There’s a good chance that you’ve probably created subclasses of UIViews for yourself. You may have views like Mapbox, or you’re using something off of CocoaPods that you like. It does not have to be a subclass of your own but you can create an extension method.
The idea is that you would be able to have this “Rx-able” custom view.
class ViewController {
let myCustomView : MyCustomView
override func viewDidLoad(){
super.viewDidLoad()
viewModel.dataStream
.bindTo(myCustomView.sink)
.addTo(disposeBag)
}
}
You create an observer. An observer is a block of code that takes in the event. The event’s an enum that returns to you its type, and then an error, and then a completion. You probably will shove a whole bunch of these “sinks” in there, and then do something by stating its property text, and then in case of an error maybe turn it red. If it’s completed, maybe make it disappear.
If you have some custom stock label or ticker price, you can create Rxable elements. This is nice because it doesn’t know about any data, or about async; it just accepts the data in that format. And it has nice generics with it as well. Make sure it is always weak within that block, because you are referencing to itself.
So you have some custom view. In your ViewModel, there’s some data stream that comes in. And then you’re going to bind to the sink. Therefore you have the ability to get a UIView to accept Reactive streams.
And the way to get it is, using CocoaPods or Carthage, RxSwift and RxCocoa. RxCocoa is the UIKit extension methods, and has it for Mac development as well. This is the big one because I told you how big UITableView and UICollectionView is.
Here is an example app (see video) that shows you the power of RxDataSources, which is a separate open-source library that is built on top of RxSwift. Here is a customization using UITableView with different sections.
I’ll show you how small the code is. You’re not even going to implement UITableView data source. You’re going to have sections. Sections are interesting. It’s just a protocol. A data source that accepts a protocol. And it’s going to have an ID on it. You can tell Rx data sources what the ID is, what is mutated, when it is different, and it will call the respective UITableView.animate row or delete row, without you having to always mutate the data source manually. You create two different sections, the first section and the second section. And then you have a data source.
The data source is going to be typed with this generic of my section. This is your custom table view implementation. You’re going to call your dequeue
reusable identifier, tell it what table view cell you want. For the ViewModel for data source, you can have the data source but tell it what section. For the static sections that we made, we bindTo(tableView.rx.items(...))
, and with the data source that it wants. Now you set the delegate to itself and you have multiple sections.
You’re not creating member variables with different values, you’re not managing indexes, index sections, and paths. You’re handing it over to RxSwift and RxDataSources. And this can be as async as you want.
The randomized example is quite powerful. There are all these sections in UICollectionView
. And there is a fake async stream that is constantly running. It’s much smaller than if you wrote it by hand. And all it’s doing is creating different numbers, removing values differently. And you never have to call the insertRow
, indexPath
and jerry-rig all that nonsense. And now you have something that is quite impressive. This would be great for a stock ticker application.
I did not talk a lot about operators; I didn’t want to overwhelm a lot of people. The next talk will be operators using complex things sort, groupBys, combining, merging, zipping, what they all mean, that will be a much more like algorithms and data structures talk about RxSwift. But this is how you get the data structure to talk to your UI in a very nice conformant way.
Q & A (27:55)
Q: Is it difficult for a shop or a company to do this? And is it harder at larger or smaller companies? How do you convince people? Max: This is a great part of RxSwift that it is not very opinionated, unlike all of a sudden introducing Lodash. You can use your own native events if you want to and you can use it in an isolated way. For example, if you have many tickets that start saying, “creating new view controllers,” you can use it. And the old code does not need to even know. You are going to have to maintain the two things in tandem but it’s a utility library, it’s not like a framework.
Q: When you added a breakpoint - I wanted to ask what performance penalty does RxSwift have? When the app gets larger and larger and you have network code, speed is very important. Max: Yes, that is a large topic in general. In terms of doing things on the main thread, there’s no issue. I have not seen the issue, there are benchmarks on RxSwift’s repo page, you can check them out.
The real trick with iOS development is if you can handle async and concurrency, and that is where RxSwift shines. It’s probably for another talk, but I can quickly go over what the benefits might actually be.
For example (see video), you can create a scheduler like concurrent. Which one do we want? You can create a background scheduler. Stock prices. And then observe on the scheduler. And then do next. Maybe map. This is going to be on that background QoS. This will be on the background QoS. Say you want to return it back to the main thread. This will run on the background, that is where you are going to probably see your performance benefits.
There are other things, for example, you probably want to do something with throttling. If you’re doing something like a search, you said you’re from Yelp? Many people do not want to click the search button in apps; they want to type and then you want to send these things off to the backend. You’re going to probably not send every single character that streams in.
Let’s say this is a stream of text that is coming in. You can do a throttle. And then do 0.25 seconds. Main scheduler. Instance. Subscribe on. And then run your async code here. Or if we are going to do it the MVVM way, you are going to bindTo. If someone is a fast typer, you do not want to search for, for example if you want to search pizza or something, you do not want to search P, that is irrelevant. You may want to throttle but not only throttle but actually run this block if he has only entered three bits of data. This helps you out in terms of maintaining your main thread to be as fast as possible.
But you should not be doing it on the main thread on the iPhone 7s, then you have serious problems. This is a fast machine. Do everything on other threads, deliver them on the right one. And you can always test this by the way, you can check it out on the repo in the testing, how people throw things on one thread then do it on another one, then hand it back to the main thread. But in your ViewModel, you usually want to almost always give it back to the main thread.
Receive news and updates from Realm straight to your inbox