prescribing rx responsibly

Post on 05-Apr-2017

66 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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 ?

7RxMarbles.com

W H A T I S R X ?

8RxMarbles.com

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!

top related