Rx in the real world 1 Rob Ciolli
2 Rob Ciolli
3 Rob Ciolli
The App 4 Rob Ciolli
Quick architecture overview 5 Rob Ciolli
MV - WTF 6 Rob Ciolli
Model Simple, immutable data struct returned from DB or APIs 7 Rob Ciolli
View UI layout as defined in storyboard file 8 Rob Ciolli
Controller Regular Cocoa ViewController protocol ViewControllerProtocol: class { ... associatedtype ViewModelType func recieve(viewModel: ViewModelType) } 9 Rob Ciolli
Presenter import UIKit protocol PresenterProtocol { associatedtype ViewControllerType: UIViewController func makeViewController() -> ViewControllerType } 10 Rob Ciolli
ViewModel protocol ViewModelDelegateProtocol { } protocol ViewModelProtocol { associatedtype DelegateType //: ViewModelDelegateProtocol var delegate: DelegateType { get } init(delegate: DelegateType) } class BaseViewModel<T> : ViewModelProtocol { let delegate: T required init(delegate: T) { self.delegate = delegate } } 11 Rob Ciolli
Example 1 Beer Detail Page 12 Rob Ciolli
Beer Model struct Beer { let id: Int let name: String let style: String let brewry: String let abv: Double let ibu: Double let description: String let image: String } 13 Rob Ciolli
Observable • next • complete • error 14 Rob Ciolli
Observer • 'Listens' to an Observable • implements onNext, onCompleted, onError • subscribing or binding returns a Disposable • DisposeBag pattern 15 Rob Ciolli
Observable example let observable = Observable<String>.create { observer in o.onNext("beer") o.onNext("is") o.onNext("good") o.onCompleted() return Disposables.create() } 16 Rob Ciolli
Observer example let disposeBag = DisposeBag() observable .subscribe( onNext: { s in print(s) }, onError: { _ in print("wtf") }, onCompleted: { _ in print("done") } ) .disposed(by: disposeBag) 17 Rob Ciolli
Mutating the streams Observable Operators • Transform => Map & FlatMap ... • Filter => Filter / Debounce / Skip / Take • Combine => Zip / CombineLatest • Error Handling => Retry / Catch 18 Rob Ciolli
Data Layer import RxSwift protocol Datalayer { func requestAllBeers() -> Observable<[Beer]> } 19 Rob Ciolli
DetailPresenter struct DetailPresenter { let beer: Beer init(beer: Beer) { self.beer = beer } } extension DetailPresenter: PresenterProtocol { ... } extension DetailPresenter: DetailViewModelDelegateProtocol { } 20 Rob Ciolli
DetailViewModel protocol DetailViewModelDelegateProtocol: ViewModelDelegateProtocol { var beer: Beer { get } } class DetailViewModel: BaseViewModel<DetailViewModelDelegateProtocol> { let name = Variable<String?>("") ... required init(delegate: DetailViewModelDelegateProtocol) { super.init(delegate: delegate) name.value = beer.name ... } } 21 Rob Ciolli
Map 22 Rob Ciolli
DetailViewController class DetailViewController: UIViewController, ViewControllerProtocol { ... let disposeBag = DisposeBag() @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() viewModel.name.asObservable() .bindTo(nameLabel.rx.text) .disposed(by: disposeBag) viewModel.image.asObservable() .map { UIImage(named: $0)! } .bindTo(imageView.rx.image) .disposed(by: disposeBag) } } 23 Rob Ciolli
Show me the code Example 1 - Beer Detail Page 24 Rob Ciolli
! " Binding is too verbose Only displays one piece of data Not really reactive 25 Rob Ciolli
Example 1b Beer Detail Page ( again... ) heaps more reactive 26 Rob Ciolli
Binding 27 Rob Ciolli
what if.... viewModel.name.asObservable() .bindTo(nameLabel.rx.text) looked like nameLabel.rx.text <- viewModel.name 28 Rob Ciolli
... and you could add all disposables to disposeBag in one call disposeBag.dispose([ nameLabel.rx.text <- viewModel.name, styleLabel.rx.text <- viewModel.style, brewryLabel.rx.text <- viewModel.brewry, ... ]) 29 Rob Ciolli
<- operator infix operator <- func <- <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable { return variable.asObservable().bind(to: property) } 30 Rob Ciolli
DisposeBag extension DisposeBag { func dispose(_ disposables: [Disposable]) { disposables.forEach { [unowned self] disposable in self.insert(disposable) } } } 31 Rob Ciolli
ViewModel Requirements Page through [ Beer ] protocol DetailViewModelDelegateProtocol: ViewModelDelegateProtocol { var beers: Observable<[Beer]> { get } ... } 32 Rob Ciolli
ViewModel Requirements React to Next/Prev let next = Variable<()>() let index: Variable<Int> ... next.asObservable().subscribe(onNext: onNext) ... private func onNext() { index.value += 1 } 33 Rob Ciolli
Combine Latest 34 Rob Ciolli
ViewModel Requirements Be heaps more reactive Observable.combineLatest(delegate.beers, index.asObservable(), resultSelector: selectBeer) 35 Rob Ciolli
ViewModel Requirements Control enabled state of UIButton extension UIButton { var rx_driveEnable: AnyObserver<Bool> { return UIBindingObserver(UIElement: self) { button, enabled in button.isUserInteractionEnabled = enabled }.asObserver() } } 36 Rob Ciolli
Show me the code (... and tests) Example 1b - Beer Detail Page 37 Rob Ciolli
Example 2 Sign in Page capture user input validate input call api and handle response 38 Rob Ciolli
Sign in ViewModel initialise protocol SigninViewModelDelegateProtocol: ViewModelDelegateProtocol { func signin(username: String, password: String) -> Observable<Void> } class SigninViewModel: BaseViewModel<SigninViewModelDelegateProtocol> { ... let username = Variable<String?>(nil) let password = Variable<String?>(nil) let signInTapped = Variable<()>() let error = Variable<String?>(nil) let signedIn = Variable<()>() 39 Rob Ciolli
Sign in ViewModel call api ... signInTapped.asObservable() .subscribe(onNext: signin) .disposed(by: disposeBag) ... private func signin() { guard let username = username.value, let password = password.value else { return } delegate.signin(username: username, password: password) .subscribe(onNext: onSignedIn, onError: onSignInError) .disposed(by: disposeBag) } private func onSignedIn() { signedIn.value = () } private func onSignInError(_: Error) { error.value = "Error signing in" } 40 Rob Ciolli
Sign in ViewModel input validation var signInEnabled: Observable<Bool> { return Observable.combineLatest(username.asObservable(), password.asObservable(), resultSelector: inputIsValid) } private func inputIsValid(username: String?, password:String?) -> Bool { guard let username = username, let password = password else { return false } return !username.isEmpty && !password.isEmpty } } 41 Rob Ciolli
Show me the code Example 2 - Beer Sign in 42 Rob Ciolli
Example 3 Beer ListView filter list show detail view 43 Rob Ciolli
List ViewModel let searchVariable = Variable<String?>("") let itemTapped = Variable<Int>(-1) required init(delegate: ListViewModelDelegateProtocol) { ... itemTapped.asObservable() .skip(1) .subscribe(onNext: onItemTapped) .disposed(by: disposeBag) } 44 Rob Ciolli
Filter 45 Rob Ciolli
List ViewModel func filteredBeers() -> Observable<[Beer]> { let search = searchVariable.asObservable() return Observable.combineLatest(delegate.beers, search, resultSelector: filterBeers) } private func filterBeers(beers: [Beer], search: String?) -> [Beer] { guard let search = search else { return beers } return beers.filter { search.isEmpty || $0.name.lowercased().contains(search.lowercased()) } } 46 Rob Ciolli
List ViewModel private func onItemTapped(index: Int) { delegate.showDetail(beers: filteredBeers(), index: index) } 47 Rob Ciolli
List ViewController viewModel.filteredBeers() .debounce(0.3, scheduler: MainScheduler.instance) .bind(to: tableView.rx.items( cellIdentifier: "Cell", cellType: UITableViewCell.self), curriedArgument: initialiseCell) 48 Rob Ciolli
Show me the code Example 3 - Beer List 49 Rob Ciolli
Wash up • single responsibility is good • Rx makes us think about data flow • infix operators and swift extensions are cool 50 Rob Ciolli
Resources • http:/ /reactivex.io/ • http:/ /rxmarbles.com/ • https:/ /github.com/ReactiveX/RxSwift 51 Rob Ciolli
Thank you @boblaroc <- ( , ) 52 Rob Ciolli
Recommend
More recommend