Rxswift: Perform work in background and update UI with Driver unit

Created on 4 Jul 2016  路  7Comments  路  Source: ReactiveX/RxSwift

Hi,

Im am facing an implementation doubt that I think it is quiet common.

Lets say I have I have a viewController that is in charge of multiplying a number by two and then showing the result in a label. For what I know about MVVM, the multiplication should be done in the ViewModel and then use bindings to connect the result to the UI. My actual implementation is as follows:

import Foundation
import RxSwift
import RxCocoa
import RxOptional

protocol viewModelProtocol {
  var numberMultipliedByTwo: Driver<Float> { get }
  func setNumber(number: Float)
}

class viewModel: viewModelProtocol {
  let numberMultipliedByTwo: Driver<Float> // Because this is going to be connected to the UI, Driver unit is used to assure the bindings are done in the main threat
  private let _numberMultipliedByTwo: Variable<Float?> = Variable(nil) // Because of access control we just expose the driver, but not the Variable holding the value (we do not want anybody from outside the viewModel to change the value)

  init() {
    numberMultipliedByTwo = _numberMultipliedByTwo.asDriver().filterNil().map {
      // Executed on subscription thread, and because the subscription is going to be done with a Driver, this is executed in MainThread
      $0 * 2
    }
  }

  func setNumber(number: Float) {
    // Does not matter the thread you are using to set the number, the multiplication is always going to be done in mainThread
    _numberMultipliedByTwo.value = number
  }
}

The only thing I do not like about the code is the multiplication being done in the mainThread: for what I understand It is preferable to do it in the background, so the main thread is not locked and the performance is better. The ideal solution for me would be:
--> Background thread
Call the function "setNumber(5)"
--> Instead changing to mainThread to perform the multiplication, keep going in the actual (background) thread
Map is executed in the same background thread -> multiplication done in background thread
-->Change to mainThread
Binding (connection to the UI) done in MainThread

Following that idea, the closer I got is this:

import Foundation
import RxSwift
import RxCocoa
import RxOptional

protocol viewModelProtocol {
  var numberMultipliedByTwo: Driver<Float> { get }
  func setNumber(number: Float)
}

class viewModel: viewModelProtocol {
  let numberMultipliedByTwo: Driver<Float> // Because this is going to be connected to the UI, Driver unit is used to assure the bindings are done in the main threat
  private let _numberMultipliedByTwo: Variable<Float?> = Variable(nil) // Because of access control we just expose the driver, but not the Variable holding the value (we do not want anybody from outside the viewModel to change the value)

  init() {
    numberMultipliedByTwo = _numberMultipliedByTwo
      .asObservable()
      .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
      .filterNil().map {
        // Executed on backgroundThread
        Optional($0 * 2)
    }.asDriver(onErrorJustReturn: nil).filterNil()
  }

  func setNumber(number: Float) {
    _numberMultipliedByTwo.value = number
  }
}

And the workflow is this:
--> Background thread (lets call this thread BGTA)
Call the function "setNumber(5)" in the thread BGTA
--> Change to the background queue specified by observeOn (lets call this thread BGTB)
Map is executed in BGTB -> multiplication done in background thread
-->Change to mainThread (thanks to Driver)
Binding (connection to the UI) done in MainThread

The things I do not like about this:

  • The code is less clear, longer and verbose.
  • There is a thread change from BGTA to BGTB that is not needed: it would be better to stay in BGTA for the multiplication (performance improvement)

So my question is: is there a way of doing this in a cleaner and more elegant way? This procedure is quiet common and in my mind I am just repeating "this is too much code for such a simple task, there must be a simpler way everybody is using".

Thanks a lot!

Most helpful comment

Hi @acecilia ,

I'm trying to understand the root problem :)

Soo ...

I'm not sure why you are using Float?, if you want to ignore error when converting to Driver, you can just do:

.asDriver(onErrorDriveWith: Driver.never())

Another thing is that you are converting to driver too late:
You would probably want to do:

_numberMultipliedByTwo.asDriver()
      .flatMapLatest { x in
               Observable.just(x)
                  .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
                  .map {
                         // Executed on backgroundThread
                         Optional($0 * 2)
                   }
                   .asDriver(onErrorDriveWith: Driver.never())
    }

to shorten this, you can create Driver extension

extension Driver {
    func flatMapOnBackground<R>(scheduler: SchedulerType, work: Element -> R) -> Driver<R> {
        return self.flatMapLatest { x in
            Observable.just(x)
                .observeOn(scheduler)
                .map(work)
                .asDriver(onErrorDriveWith: Driver<R>.never())
        }
    }
}

and use it

let myBusyScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background)
_numberMultipliedByTwo.asDriver()
      .flatMapOnBackground(myBusyScheduler) { $0 * 2 }

All 7 comments

The use of a private variable and an internal driver I think is related to https://github.com/ReactiveX/RxSwift/pull/697

Hi~ @acecilia

Maybe this code is better:

class ViewModel {

    let numberMultipliedByTwo: Observable<Float>

    init(number: Observable<Float>) {
        numberMultipliedByTwo = number
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .map { $0 * 2 }
            .observeOn(MainScheduler.instance)
    }
}

hi @acecilia
remove option.
if you want to change value in viewmodel,private _numberMultipliedByTwo is worked, private let _numberMultipliedByTwo: Variable = Variable(nil) replace with private let _numberMultipliedByTwo: Variable = Variable(0) . use skip(1) to ignore first value.

Thanks for your responses guys. @DianQK, if your viewModel has a high number of observables the init method becomes unmanageable: too long. And the problems with threads are still there. I do not think your solution fits for me in this case.
@FengDeng ok, I could do that, the result is similar.

Hi @acecilia ,

I'm trying to understand the root problem :)

Soo ...

I'm not sure why you are using Float?, if you want to ignore error when converting to Driver, you can just do:

.asDriver(onErrorDriveWith: Driver.never())

Another thing is that you are converting to driver too late:
You would probably want to do:

_numberMultipliedByTwo.asDriver()
      .flatMapLatest { x in
               Observable.just(x)
                  .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
                  .map {
                         // Executed on backgroundThread
                         Optional($0 * 2)
                   }
                   .asDriver(onErrorDriveWith: Driver.never())
    }

to shorten this, you can create Driver extension

extension Driver {
    func flatMapOnBackground<R>(scheduler: SchedulerType, work: Element -> R) -> Driver<R> {
        return self.flatMapLatest { x in
            Observable.just(x)
                .observeOn(scheduler)
                .map(work)
                .asDriver(onErrorDriveWith: Driver<R>.never())
        }
    }
}

and use it

let myBusyScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background)
_numberMultipliedByTwo.asDriver()
      .flatMapOnBackground(myBusyScheduler) { $0 * 2 }

This is proooobably resolved ....

I had it pending, thinking about adding it to the Addons repo. Anyway, thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kzaher picture kzaher  路  3Comments

gaudecker picture gaudecker  路  3Comments

RafaelPlantard picture RafaelPlantard  路  3Comments

apoloa picture apoloa  路  3Comments

trant picture trant  路  3Comments