MVVM with RxSwift
Maximilian Alexander
https://goo.gl/stQQDO
View This Live with Me!
What Does It Solve?
MVC Pattern
Massive
View
Controller
Instead of
Shoving EVERYTHING into your ViewController
-
Model
-
View
-
ViewModel
View
- UIButton
- UITextField
- UITableView
- UICollectionView
- NSLayoutConstraints
View
class LoginViewController : UIViewController {
@IBOutlet weak var usernameTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmButton: UIButton
}
Code So Far
Model
- CLLocationManager
- Alamofire
- Facebook SDK
- Database like Realm
- Service Classes
Model
The Model is just an abstract word for
"your data"
ViewModel
1. Prepares Data
2. Manipulates Data
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
}
}
}
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()
}
}
•UITextField
•UITextField
•UIButton
ViewModel
Model
•tapFunction()
•doSomething()
Hooking UI to ViewModel and Back
Not as simple as you think
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()
}
}
That looked less fun
What about UI that reacts to changing events?
Enabled/Disable Confirm Button
aka.
Form Validation
•UITextField
•UITextField
•UIButton
ViewModel
Model
•tapFunction()
•doSomething()
This is Hard
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
}
}
Now how do we hookup isValid to confirmButton?
Important!
ViewModel does not hold a reference to the ViewController!
struct LoginViewModel {
var username: String = "" {
didSet {
evaluateValidity()
}
}
var password: String = "" {
didSet {
evaluateValidity()
}
}
var isValid : Bool = "" {
didSet {
loginViewController?.confirmButton.isDisabled = !isValid
}
}
weak var loginViewController : LoginViewController?
func attemptToLogin() {
//truncated for space
}
private func evaluateValidity(){
isValid = username.characters.count > 0
&& password.characters.count > 0
}
}
💀 Never do this!
View Model DONTS
- Reference View Controller
- Don't import UIKit
- Reference anything from UIKit
- Data should be going in and out
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
}
}
class LoginViewController {
@IBOutlet var confirmButton: UIButton!
var loginViewModel = LoginViewModel()
override func viewDidLoad(){
super.viewDidLoad()
loginViewModel.isValidCallback = { [weak self] (isValid) in
self?.confirmButton.isEnabled = isValid
}
}
}
Listen to the Callback
That's it for MVVM!
Unidirectional Data Flow with MVVM is hard
What we want is something like this:
UIKit
ViewModel
Model
How can RxSwift help with this?
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
}
}
}
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)
}
}
That's How Easy It Is!
Make sure the UIBindings Don't Talk to Each Other
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)
}
}
ViewController is still massive 😔
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
}
}
}
}
What if RxSwift + RxCocoa doesn't have bindings
class ViewController {
let myCustomView : MyCustomView
override func viewDidLoad(){
super.viewDidLoad()
viewModel.dataStream
.bindTo(myCustomView.sink)
.addTo(disposeBag)
}
}
Custom Binding Sinks
Looking Forward
pod 'RxSwift'
pod 'RxCocoa'
#the good stuff
pod 'RxDataSource'
MVVM with RxSwift
By Max Alexander
MVVM with RxSwift
- 2,509