$30 off During Our Annual Pro Sale. View Details »

RxSwift: Deep Cuts

RxSwift: Deep Cuts

Krzysztof Siejkowski

October 21, 2017
Tweet

More Decks by Krzysztof Siejkowski

Other Decks in Programming

Transcript

  1. RxSwift: 

    Deep Cuts
    Krzysztof Siejkowski

    View Slide

  2. Alex is
    an iOS developer

    View Slide

  3. Reactive
    programming

    View Slide

  4. RxSwift

    View Slide

  5. View Slide

  6. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  7. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  8. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  9. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  10. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  11. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  12. #1
    What references 

    does it carry?

    View Slide

  13. Observable
    .just(42)
    Just

    View Slide

  14. Observable
    .just(42)
    .filter { $0 > 30 }
    Just
    Filter

    View Slide

  15. Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    Just
    Filter
    Map

    View Slide

  16. Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged {
    $0 !== $1
    }
    Just
    Filter
    Map
    Distinct
    UntilChanged

    View Slide

  17. let observable:
    Observable =
    Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged {
    $0 !== $1
    }
    Just
    Filter
    Map
    observable
    observable

    View Slide

  18. environment
    Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged {
    $0 !== $1
    }
    Just
    Filter
    Map
    Distinct
    UntilChanged
    filter
    closure
    compare
    closure
    map
    closure

    View Slide

  19. Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }
    Just
    Filter
    Map
    Distinct
    UntilChanged
    filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink

    View Slide

  20. Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }
    Just
    Filter
    Map
    Distinct
    UntilChanged
    filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink

    View Slide

  21. Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }
    Just
    Filter
    Map
    Distinct
    UntilChanged
    filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Observer

    View Slide

  22. filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Just Sink
    Disposer
    Filter Sink
    Disposer
    Map Sink
    Disposer
    DUC Sink
    Disposer
    Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }

    View Slide

  23. reference
    cycles
    filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Just Sink
    Disposer
    Filter Sink
    Disposer
    Map Sink
    Disposer
    DUC Sink
    Disposer

    View Slide

  24. filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Just Sink
    Disposer
    Filter Sink
    Disposer
    Map Sink
    Disposer
    disposable
    let disposable:
    Disposable =
    Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }

    View Slide

  25. disposable
    .dispose()
    filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Just Sink
    Disposer
    Filter Sink
    Disposer
    Map Sink
    Disposer
    disposable

    View Slide

  26. .dispose()
    — called externally 

    on subscription disposable
    — called internally 

    when event stream ends

    View Slide

  27. filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Just Sink
    Disposer
    Filter Sink
    Disposer
    Map Sink
    Disposer
    disposable
    observe
    closure
    let disposable =
    Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }

    View Slide

  28. environment
    let disposable =
    Observable
    .just(42)
    .filter { $0 > 30 }
    .map { “\($0)” }
    .distinctUntilChanged
    {
    $0 !== $1
    }
    .subscribe {
    print($0)
    }
    filter
    closure
    compare
    closure
    map
    closure
    Just
    Sink
    Filter
    Sink
    Map
    Sink
    DUC
    Sink
    Observer
    Just Sink
    Disposer
    Filter Sink
    Disposer
    Map Sink
    Disposer
    disposable
    observe
    closure

    View Slide

  29. Memory leaks
    and
    extended lifetime

    View Slide

  30. Dispose your
    subscriptions

    View Slide

  31. Watch your
    references

    View Slide

  32. observable
    .map { [weak self] in
    self!?.transform($0)
    }
    .subscribe {
    [weak self] in
    self!?.react(to: $0)
    }
    observable
    .map { [unowned self] in
    self.transform($0)
    }
    .subscribe {
    [unowned self] in
    self.react(to: $0)
    }
    .disposed(
    by: self.disposeBag
    )

    View Slide

  33. !// instance method
    func foo(_ bar: Int) !-> Int
    !// instance scope
    Observable.just(42)
    .map(foo)
    !// instance method
    func foo(_ bar: Int) !-> Int
    !// instance scope
    Observable.just(42)
    .map { self.foo($0) }

    View Slide

  34. dataProvider.refreshData()
    .subscribe(
    onNext: { [weak self] in
    self!?.update(with: $0)
    },
    onError: { [weak self] in
    if let error = $0 as? ReasonableError {
    self!?.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  35. dataProvider.refreshData()
    .subscribe(
    onNext: { [unowned self] in
    self.update(with: $0)
    },
    onError: { [unowned self] in
    if let error = $0 as? ReasonableError {
    self.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  36. dataProvider.refreshData()
    .subscribe(
    onNext: { [unowned self] in
    self.update(with: $0)
    },
    onError: { [unowned self] in
    if let error = $0 as? ReasonableError {
    self.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  37. #2
    What thread does it
    do the work on?

    View Slide

  38. Schedulers
    define context
    of work execution

    View Slide

  39. ImmediateSchedulerType
    func schedule(
    _ state: StateType,
    action: @escaping (StateType) !-> Disposable
    ) !-> Disposable


    View Slide

  40. MainScheduler
    let mainQueue = DispatchQueue.main
    !// simplified essence of schedule method
    if DispatchQueue.isMain {
    action(state)
    } else {
    mainQueue.async {
    action(state)
    }
    }

    View Slide

  41. SerialDispatchQueueScheduler
    !// internal serial dispatch queue of given properties
    let queue = DispatchQueue.global(qos: qos.qosClass)
    !// simplified essence of schedule method
    queue.async {
    action(state)
    }

    View Slide

  42. OperationQueueScheduler
    !// operation queue provided by client in the initializer
    let operationQueue: OperationQueue
    !// simplified essence of schedule method
    operationQueue.addOperation(
    BlockOperation {
    action(state))
    }
    )

    View Slide

  43. Also in schedulers
    • QOS
    • serial / concurrent
    • immediate / delayed
    • driven by client

    View Slide

  44. Scheduler-using operators
    interval(1, scheduler)
    delay(2, scheduler)
    throttle(3, scheduler)

    View Slide

  45. Scheduler-agnostic operators
    map { foo($0) }
    flatMap { bar($0) }
    filter { $0 !== wanted }

    View Slide

  46. Scheduler-defining operators
    observeOn(scheduler)
    subscribeOn(scheduler)

    View Slide

  47. Observable
    .just(42)
    .filter { $0 > 33 }
    .map { "\($0)" }
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    Subscription
    thread

    View Slide

  48. Observable
    .just(42)
    .observeOn(greenScheduler)
    .filter { $0 > 33 }
    .map { "\($0)" }
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    Subscription
    thread
    Green scheduler

    View Slide

  49. Observable
    .just(42)
    .observeOn(greenScheduler)
    .filter { $0 > 33 }
    .observeOn(redScheduler)
    .map { "\($0)" }
    .observeOn(greenScheduler)
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    Subscription
    thread
    Red scheduler
    Green scheduler
    Green scheduler

    View Slide

  50. Observable
    .just(42)
    .observeOn(greenScheduler)
    .filter { $0 > 33 }
    .observeOn(redScheduler)
    .subscribeOn(blueScheduler)
    .map { "\($0)" }
    .observeOn(greenScheduler)
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    Blue scheduler
    Red scheduler
    Green scheduler
    Green scheduler

    View Slide

  51. Observable
    .just(42)
    .observeOn(greenScheduler)
    .filter { $0 > 33 }
    .subscribeOn(blueScheduler)
    .observeOn(redScheduler)
    .subscribeOn(redScheduler)
    .map { "\($0)" }
    .observeOn(greenScheduler)
    .subscribeOn(greenScheduler)
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    Blue scheduler
    Red scheduler
    Green scheduler
    Green scheduler

    View Slide

  52. Observable
    .just(
    42, scheduler: blueScheduler
    )
    .observeOn(greenScheduler)
    .filter { $0 > 33 }
    .observeOn(redScheduler)
    .subscribeOn(redScheduler)
    .map { "\($0)" }
    .observeOn(greenScheduler)
    .subscribe { print($0) }
    .disposed(by: disposeBag)
    Blue scheduler
    Red scheduler
    Green scheduler
    Green scheduler

    View Slide

  53. Schedulers used for:
    • generation
    • internal work

    View Slide

  54. Schedule-using for generation
    !// Scheduler to send elements on.
    .fromScheduled([1, 2, 3], greenScheduler)
    !// Scheduler to run the producer loop on.
    .repeatElement(1, blueScheduler)
    !// Scheduler to run the timer on.
    .interval(1, redScheduler)

    View Slide

  55. Schedule-using for internal work
    !// Scheduler to run the throttle timers on.
    .throttle(1, blueScheduler)
    !// Scheduler to run the subscription delay timer on.
    .delay(1, greenScheduler)
    !// Scheduler to run buffering timers on.
    .buffer(timeSpan: 1, count: 3, scheduler: redScheduler)

    View Slide

  56. Delivery thread
    may change
    between events

    View Slide

  57. Observable
    .merge([
    Observable.interval(3, redScheduler),
    Observable.interval(5, greenScheduler),
    ])
    .subscribe { [unowned self] in
    !// sometimes red, sometimes green
    self.work(with: $0)
    }
    .disposed(by: disposeBag)

    View Slide

  58. Read the docs 

    and 

    the source code

    View Slide

  59. When in doubt,
    ensure with
    observeOn

    View Slide

  60. RxCocoa traits
    guarantee
    MainScheduler

    View Slide

  61. dataProvider.refreshData()
    .observeOn(MainScheduler.instance)
    .subscribe(
    onNext: { [unowned self] in
    self.update(with: $0)
    },
    onError: { [unowned self] in
    if let error = $0 as? ReasonableError {
    self.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  62. dataProvider.refreshData()
    .observeOn(MainScheduler.instance)
    .subscribe(
    onNext: { [unowned self] in
    self.update(with: $0)
    },
    onError: { [unowned self] in
    if let error = $0 as? ReasonableError {
    self.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  63. #3
    What protocol
    does it follow?

    View Slide

  64. .next(data)
    .completed
    .error(error)

    View Slide

  65. Different protocols
    may need different
    subscriptions

    View Slide

  66. Does it
    finish?

    View Slide

  67. Finishing
    !// you may ignore disposable (though I recommend not to!)
    _ = observable
    .subscribe(onNext: { [weak self] in
    self!?.work(on: $0)
    })

    View Slide

  68. Not finishing
    let disposable = observable
    .subscribe(onNext: { [weak self] in
    self!?.work(on: $0)
    })
    !// ensure subscription disposing
    disposable.dispose()
    disposable.disposed(by: disposeBag)

    View Slide

  69. How long 

    does it last?

    View Slide

  70. One-off
    let serial = SerialDisposable()
    !// call each time you want the fresh data
    func fetchFreshData() {
    serial.disposable = observable
    .subscribe(onNext: { [unowned self] in
    self.work(on: $0)
    })
    }

    View Slide

  71. Updating
    let disposeBag = DisposeBag()
    !// call only once in the object lifetime
    func listenForFreshData() {
    observable
    .subscribe(onNext: { [unowned self] in
    self.work(on: $0)
    })
    .disposed(by: disposeBag)
    }

    View Slide

  72. When and how
    does it 

    start emitting?

    View Slide

  73. Immediately & synchronously
    !// if not ready to handle data at the `subscribe` time
    dataProvider
    .data()
    .skip(1)
    .subscribe(onNext: { [unowned self] in
    self.work(on: $0)
    })
    .disposed(by: disposeBag)

    View Slide

  74. After some time & asynchronously
    !// provide separate method for fetching initial data
    let initialData = dataProvider.getInitialData()
    dataProvider
    .dataUpdates()
    .subscribe(onNext: { [unowned self] in
    self.work(on: $0)
    })
    .disposed(by: disposeBag)

    View Slide

  75. Does it cache 

    the events?

    View Slide

  76. Not caching
    let observable = dataProvider
    .fetchHugeAmountOfDataFromNetwork()
    !// costly network request with fresh data
    observable.subscribe()
    !// costly network request with fresh data
    observable.subscribe()

    View Slide

  77. Caching
    let observable = dataProvider
    .fetchHugeAmountOfDataFromNetwork()
    .shareReplay(1)
    !// costly network request with fresh data
    observable.subscribe()
    !// no network refresh but also old data
    observable.subscribe()

    View Slide

  78. Also in protocols
    • error types
    • time relations
    • many more

    View Slide

  79. Expose the protocol
    via the type system

    View Slide

  80. RxCocoa traits
    • Driver
    • Signal
    • ControlProperty

    View Slide

  81. RxSwift traits
    • Single
    • Completable
    • Maybe

    View Slide

  82. Document
    the protocol

    View Slide

  83. !/*
    Returns an Observable that emits at most three times,
    starting with the first event emitted immediately
    and synchronously upon subscription.
    Times of other two events are not guaranteed.
    May not complete, but never errors out.
    Doesn’t cache any data.
    !*/
    public func thirdTimeLucky() !-> Observable

    View Slide

  84. Conventionalize
    the protocol

    View Slide

  85. final class DataProvider {
    private let proxySubject = PublishSubject()
    var data: Observable {
    return proxySubject.asObservable()
    }
    func refreshData() !-> Observable {
    return networkService
    .requestData()
    .do(onNext: { proxySubject.onNext($0) }
    .map { _ in }
    }

    View Slide

  86. Limit the scope
    of the protocol

    View Slide

  87. Code distance

    View Slide

  88. Network
    Persistence
    Domain services
    View Models
    View
    long
    code
    distance

    View Slide

  89. Network
    Persistence
    Domain services
    View Models
    View
    short
    code
    distance

    View Slide

  90. Short code distance
    !==
    easier reasoning

    View Slide

  91. Alex has found 

    the bug

    View Slide

  92. dataProvider.refreshData()
    .observeOn(MainScheduler.instance)
    .subscribe(
    onNext: { [unowned self] in
    self.update(with: $0)
    },
    onError: { [unowned self] in
    if let error = $0 as? ReasonableError {
    self.showUserMessage(with: error.reason)
    }
    }
    )
    .disposed(by: disposeBag)

    View Slide

  93. View Slide

  94. RxSwift

    View Slide

  95. Thank you 

    and happy coding!
    siejkowski.net

    View Slide