38
PRESCRIBING RX RESPONSIBLY 2017

Prescribing RX Responsibly

Embed Size (px)

Citation preview

Page 1: Prescribing RX Responsibly

PRESCRIBING RX RESPONSIBLY 💊

2 0 1 7

Page 2: Prescribing RX Responsibly

2

AGENDA

01RX INTRO

02WHEN TO USE RX (OR NOT)

03RX BEST PRACTICES

04CONCLUSION AND TAKEAWAYS

Page 3: Prescribing RX Responsibly

WHAT IS RX?

Page 4: Prescribing RX Responsibly

4

today’s talk

not today’s talk

RX-BERG

Page 5: Prescribing RX Responsibly

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

Page 6: Prescribing RX Responsibly

W H A T I S R X ?

6

STREAMS

Observable<WaterMolecule>

Observable<Bool>

Observable<MeetUp>

of things. One thing at a time.

Page 7: Prescribing RX Responsibly

W H A T I S R X ?

7RxMarbles.com

Page 8: Prescribing RX Responsibly

W H A T I S R X ?

8RxMarbles.com

Page 9: Prescribing RX Responsibly

W H A T I S R X ?

9

Observable

RX ECOSYSTEM

Variable

Subject

PublishSubject

Driver

DisposeBag

BehaviorSubject

Observer

Page 10: Prescribing RX Responsibly

W H A T I S R X ?

10

Observable

RX ECOSYSTEM

Variable

Subject

PublishSubject

Driver

DisposeBag

BehaviorSubject

Observer

Page 11: Prescribing RX Responsibly

WHEN TO USE RX

Page 12: Prescribing RX Responsibly

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 🍝

Page 13: Prescribing RX Responsibly

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

Page 14: Prescribing RX Responsibly

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 }

Page 15: Prescribing RX Responsibly

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() } }

Page 16: Prescribing RX Responsibly

SCROLLING UNDER

S C R O L L V I E W

Page 17: Prescribing RX Responsibly

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)

Page 18: Prescribing RX Responsibly

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:

Page 19: Prescribing RX Responsibly

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

Page 20: Prescribing RX Responsibly

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:

Page 21: Prescribing RX Responsibly

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) }

Page 22: Prescribing RX Responsibly

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):

Page 23: Prescribing RX Responsibly

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:

Page 24: Prescribing RX Responsibly

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 } }

Page 25: Prescribing RX Responsibly

L O O K S G R E A T B U T …

25

STACKTRACE HELL

Page 26: Prescribing RX Responsibly

RX BEST PRACTICES

Page 27: Prescribing RX Responsibly

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:

Page 28: Prescribing RX Responsibly

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

Page 29: Prescribing RX Responsibly

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.

Page 30: Prescribing RX Responsibly

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:

Page 31: Prescribing RX Responsibly

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

Page 32: Prescribing RX Responsibly

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

Page 33: Prescribing RX Responsibly

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

Page 34: Prescribing RX Responsibly

CONCLUSIONS

Page 35: Prescribing RX Responsibly

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

Page 36: Prescribing RX Responsibly

36

CLOSING THOUGHTS

C O N C L U S I O N S

© Christian Howland

Page 37: Prescribing RX Responsibly

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

Page 38: Prescribing RX Responsibly

THANKS!