Date post: | 05-Apr-2017 |
Category: |
Software |
Upload: | nareg-khoshafian |
View: | 66 times |
Download: | 0 times |
PRESCRIBING RX RESPONSIBLY 💊
2 0 1 7
2
AGENDA
01RX INTRO
02WHEN TO USE RX (OR NOT)
03RX BEST PRACTICES
04CONCLUSION AND TAKEAWAYS
WHAT IS RX?
4
today’s talk
not today’s talk
RX-BERG
W H A T I S R X ?
5
•RxSwift - Swift implementation of ReactiveX
•Follows the “Observer pattern”
•Declarative way of defining the data flow in your app
•Avoid “callback hell”
•Data flow is handled via manageable streams
W H A T I S R X ?
6
STREAMS
Observable<WaterMolecule>
Observable<Bool>
Observable<MeetUp>
of things. One thing at a time.
W H A T I S R X ?
9
Observable
RX ECOSYSTEM
Variable
Subject
PublishSubject
Driver
DisposeBag
BehaviorSubject
Observer
W H A T I S R X ?
10
Observable
RX ECOSYSTEM
Variable
Subject
PublishSubject
Driver
DisposeBag
BehaviorSubject
Observer
WHEN TO USE RX
12
01User actions (button taps, text field delegates)
02Async operations (Network calls, processing)
03Bindings (VC!!<-> VM !!<-> Model)
L I S T
WHEN TO USE RX
04Prevent code 🍝
B U T T O N A C T I O N
13
WITHOUT RX
@IBAction func logoTapped(_ sender: UIButton) { dismissUntilHome() }
navBar.logoButton !=> dismissUntilHome !!>>> rx_disposeBag
WITH RX
Drag and drop to create IBAction function. A bit more complicated if it is nested in a custom view.
We are using Fira Code font: https://github.com/tonsky/FiraCode
D A T E P I C K E R
14
WITH RX
WITHOUT RX
Drag and drop to create IBAction function. A bit more complicated if it is nested in a custom view, or number of date pickers are not constant.
datePicker.rx.date !=> viewModel.endDate !!>>> rx_disposeBag
@IBAction func datePicked(_ sender: UIDatePicker) { viewModel.endDate = sender.date }
T E X T F I E L D
15
WITH RX
titleField.textView.rx.text.orEmpty !!<-> viewModel.title !!>>> rx_disposeBag
Create binding in view controller.
WITHOUT RX
Set up delegate for the text field to listen for edit events to update view model, and manually trigger UI update when view model’s property has changed.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String)
var title: String = "" { didSet { updateTextFields() } }
SCROLLING UNDER
S C R O L L V I E W
S C R O L L V I E W
17
WITH RX
tableView.rx_scrolledUnderTop !=> viewModel.showTopGradient !!>>> rx_disposeBag tableView.rx_scrolledUnderBottom !=> viewModel.showBottomGradient !!>>> rx_disposeBag
Create binding in view controller.
WITHOUT RX
Set up delegate extensions and do the calculation within the method, at multiple places for multiple classes:
func scrollViewDidScroll(_ scrollView: UIScrollView)
P A G I N A T I O N
18
SET UP DATA CONTROLLER
func getPaginatedData<T: RealmSwift.Object>(resource: Resource, loadNextPageTrigger: Observable<Void>, dataParser: @escaping (Data) !-> ([T], Int)) !-> Observable<[T]> { let existingObjects: [T] = Realm.ip_objects(type: T.self)!?.toArray() !?? [] return recursiveGetPaginatedData(resource: resource, lastModified: lastModifiedDate, dataParser: dataParser, loadedSoFar: [], page: 1, loadNextPageTrigger: loadNextPageTrigger).startWith(existingObjects) }
func recursiveGetPaginatedData<T: RealmSwift.Object>(resource: Resource, dataParser: @escaping (Data) !-> ([T], Int), loadedSoFar: [T], page: Int, loadNextPageTrigger: Observable<Void>) !-> Observable<[T]> { guard let urlRequest = URLRequest(builder: URLRequestBuilder(resource: resource, paginationPage: page, authenticationToken = authenticationToken) else { return Observable.just(loadedSoFar) } return networkOperationQueue.add(dataRequest: urlRequest).observeOn(MainScheduler.instance) .flatMap { data !-> Observable<[T]> in var justLoaded = loadedSoFar let (models, paginationTotalItems) = dataParser(data) justLoaded.append(contentsOf: models) if justLoaded.count !== paginationTotalItems { Realm.ip_add(justLoaded, update: true, configuration: self.realmConfiguration) return Observable.just(justLoaded) } return Observable.concat([ Observable.just(justLoaded), Observable.never().takeUntil(loadNextPageTrigger), Observable.deferred { self.recursiveGetPaginatedData(resource: resource, dataParser: dataParser, loadedSoFar: justLoaded, page: page + 1, loadNextPageTrigger: loadNextPageTrigger) } ]) } }
Functions of the network call in data controller:
P A G I N A T I O N
19
SET UP VIEW MODEL
func opportunities(loadNextPageTrigger: Observable<Void>) !-> Observable<[OpportunityModel]> { return getPaginatedData(resource: Resource.opportunities, loadNextPageTrigger: loadNextPageTrigger) { (data) !-> ([OpportunityRealmModel], Int) in let opportunitiesModel = try! OpportunitiesModel(node: data) return (opportunitiesModel.opportunities, opportunitiesModel.total) } .map { $0 as [OpportunityModel] } }
Function of the API call in data controller:
Where we make the API call in view model:
dataController.opportunities(loadNextPageTrigger: nextPageTrigger.asObservable()) .map { $0.map { OpportunityCellViewModel(opportunity: $0) } } .subscribe( onNext: { self.opportunityCellViewModels = $0 self.hasMoreOpportunities = true }, onError: { Logger.error($0) NotificationCenter.postMessage(type: .requestFailure) self.hasMoreOpportunities = false }, onCompleted: { self.opportunityCellViewModels.append(EndOfListViewModel()) self.hasMoreOpportunities = false }) !!>>> rx_disposeBag
P A G I N A T I O N
20
GET NEXT PAGE IN VIEW MODEL
func nextPage() { nextPageTrigger.fire() }
How we get the next page in the view model:
N E T W O R K C A L L S
21
CHAINED NETWORK CALLS
guard let s3Object = requestS3Object(for: .opportunity) else { return nil }
return s3Object.observeOn(MainScheduler.instance).flatMap { s3Object !-> Observable<Bool> in opportunity.imageURL = URL(string: s3Object.publicURL) opportunity.imageKey = s3Object.key guard let presignedURL = URL(string: s3Object.presignedURL) else { return Observable.error(RxURLSessionError.requestCreationError) } return self.uploadImage(data: imageData, to: presignedURL) }.observeOn(MainScheduler.instance).flatMap { imageUploadSuccess !-> Observable<Data> in requestBuilder.data = opportunity.toJson() guard let urlRequest = URLRequest(builder: requestBuilder) else { return Observable.error(RxURLSessionError.requestCreationError) } return self.networkOperationQueue.add(dataRequest: urlRequest) }
R E A C H A B I L I T Y
22
CREATE REACHABILITY SERVICE
class DefaultReachabilityService: ReachabilityService { private let _reachabilitySubject: BehaviorSubject<ReachabilityStatus> var reachability: Observable<ReachabilityStatus> { return _reachabilitySubject.asObservable() } let _reachability: Reachability init() throws { guard let reachabilityRef = Reachability() else { throw ReachabilityServiceError.failedToCreate } let reachabilitySubject = BehaviorSubject<ReachabilityStatus>(value: .unreachable) let backgroundQueue = DispatchQueue(label: "reachability.wificheck") reachabilityRef.whenReachable = { reachability in backgroundQueue.async { reachabilitySubject.on(.next(.reachable(viaWiFi: reachabilityRef.isReachableViaWiFi))) } } reachabilityRef.whenUnreachable = { reachability in backgroundQueue.async { reachabilitySubject.on(.next(.unreachable)) } } try reachabilityRef.startNotifier() _reachability = reachabilityRef _reachabilitySubject = reachabilitySubject } }
How we create observable for reachability of network (by Krunoslav Zaher):
R E A C H A B I L I T Y
23
DISPLAY REACHABILITY MESSAGE
reachabilityService.reachability .skip(1) .throttle(10, scheduler: MainScheduler.instance) .observeOn(MainScheduler.instance) .subscribe(onNext: { $0.reachable ? self.hideMessage() : self.showMessage(.lostConnection) }) !!>>> disposeBag
How we subscribe to reachability observable:
B L U E T O O T H
24
SUBSCRIBING TO A BLUETOOTH STREAM
class AwesomeViewController: UIViewController { let viewModel = DeviceStatusViewModel()
@IBOutlet weak var batteryImageView: UIImageView!
func viewDidLoad() { bindToViewModel() }
override func bindToViewModel() { super.viewDidLoad()
viewModel.devicesManager.batteryStatus .subscribeOn(MainScheduler.instance) .subscribe(next: { batteryStatus in self.batteryImageView.image = self.batteryImageForStatus(batteryStatus) }) !!>>> rx_diposeBag } }
L O O K S G R E A T B U T …
25
STACKTRACE HELL
RX BEST PRACTICES
B E S T P R A C T I C E S
27
infix operator !=> : Binding infix operator !!>>> : Binding
public func !=> <T, P: ObserverType>(left: Variable<T>, right: P) !-> Disposable where P.E !== T { return left.asObservable().bindTo(right) }
public func !=> (left: UIButton, right: @escaping () !-> Void) !-> Disposable { return left.rx.tap.subscribe(onNext: { right() }) }
CREATE OPERATORS FOR COMMON TASKSSyntax sugar that greatly reduces boilerplate code:
B E S T P R A C T I C E S
28
public func !!<-> <T>(property: ControlProperty<T>, variable: Variable<T>) !-> Disposable { let bindToUIDisposable = variable .asObservable() .bindTo(property)
let bindToVariable = property .subscribe( onNext: { n in variable.value = n }, onCompleted: { bindToUIDisposable.dispose() } ) return Disposables.create(bindToUIDisposable, bindToVariable) }
TWO-WAY BINDING
S C R O L L V I E W
29
SCROLL VIEW EXTENSIONS (AS PROMISED)
extension UIScrollView { public var rx_scrolledUnderTop: Observable<Bool> { return self.rx.contentOffset .map { $0.y > 0 } .distinctUntilChanged() }
public var rx_scrolledUnderBottom: Observable<Bool> { return self.rx.contentOffset .map { $0.y < self.contentSize.height - self.frame.size.height - 1 } .distinctUntilChanged() } }
Create extension for scroll view.
B E S T P R A C T I C E S
30
cell.viewOpportunityOverlayView.rx_tapGesture !=> { self.showOpportunityDetail(opportunityVM.opportunity) } !!>>> cell.cellDisposeBag
WATCH OUT FOR CELL REUSEBe sure to reset bindings on cell reuse! In view controller:
override func prepareForReuse() { super.prepareForReuse() cellDisposeBag = DisposeBag() }
In table view cell:
B E S T P R A C T I C E S
31
func bindToViewModel() { Observable.combineLatest(vm.passwordValid, vm.passwordIsMinLength) { $0 !&& $1 } !=> passwordReqsLabel.rx_hidden !!>>> rx_disposeBag
vm.emailAddress !<- emailAddressField.rx_text !!>>> rx_disposeBag vm.password !<- passwordField.rx_text !!>>> rx_disposeBag vm.passwordConfirmation !<- confirmPasswordField.rx_text !!>>> rx_disposeBag }
@IBOutlet weak var settingsButton: UIButton! { didSet { settingsButton !=> showSettingsVC !!>>> rx_disposeBag } }
DESIGNATED METHOD FOR BINDING
B E S T P R A C T I C E S
32
class DeviceManager {
private var batteryStatus = Variable<BatteryLevel>(.low)
public var batteryStatusObs = batteryStatus.asObservable()
}
PUBLIC VS. PRIVATE
B E S T P R A C T I C E S
33
extension ObservableType {
public func ip_repeatingTimeouts( interval dueTime: RxTimeInterval,
element: E, scheduler: SchedulerType = MainScheduler.instance ) !-> Observable<E> {
return Observable.of( self.asObservable(), debounce(dueTime, scheduler: scheduler).map { _ in element } )
.merge() } }
REPEATING TIMEOUTS
CONCLUSIONS
35
• What are you reacting to?
• Are you using a struct or a class?
• Observable vs. Variable?
• Does the subscription need to update things on the screen?
• Will the view update while it’s being displayed?
ASK YOURSELF…
C O N C L U S I O N S
36
CLOSING THOUGHTS
C O N C L U S I O N S
© Christian Howland
37
• RxMarbles.com
• ReactiveX.io
• https://github.com/IntrepidPursuits/swift-wisdom
• https://github.com/ReactiveX/RxSwift
• rxswift.slack.com
USEFUL LINKS
C O N C L U S I O N S
THANKS!