Rxswift: Could be that Driver is not working as expected?

Created on 15 Jun 2016  路  3Comments  路  Source: ReactiveX/RxSwift

I have red the documentation about Driver and I realized that it is the best option to connect my observables (now changed to drivers) to my UI elements because they warranty me that the connection is being done in the Main thread (or at least that is what I understood ;P)

I tried it just to check and this is what I got:

    // self.avatar is and UIImageView
    let variable: Variable<UIImage?> = Variable(UIImage())
    let driver = variable.asDriver()

    // Execute in background thread
    Async.background {
      /* Breaks with:
       fatal error: Executing on backgound thread.Please use `MainScheduler.instance.schedule` to schedule work on main thread.: file / Users / andres / Git / Worktoday / Worktoday / Pods / RxSwift / RxSwift / Rx.swift, line 23
       */
      driver.drive(self.avatar.rx_image).addDisposableTo(self.disposeBag)

      /*Does not break, runs properly*/
      variable.asObservable().observeOn(MainScheduler.instance).bindTo(self.avatar.rx_image).addDisposableTo(self.disposeBag)
    }

I was expecting the drive function to change to the main thread and run without breaking.
Am I missing something?
Thanks!

Most helpful comment

Hi @acecilia ,

You need to call drive* from main thread. Everything documented is true. Driver does indeed have observeOn(MainScheduler.instance) internally to ensure elements are received on main thread.

It is easy to verify that source code for ObservableType.asDriver* methods contains observeOn(MainScheduler.instance) to make sure all event are emitted on main scheduler.

The reason why you need to also call drive method from main thread is because it also has shareReplay operator that is used as a sharing strategy that replays latest element immediately upon subscription on the same thread which is calling drive* method.

This is the scenario:

let xs: Observable<[DatabaseEntity]> = ....
let xsDriver: Driver<[DataBaseEntity]> = xs.asDriver(onErrorJustReturn: [])

// this is on main thread
xsDriver.driveNext { entities in
    print("E1 \(entities)")
}

// prints E1 [....

// so now somebody does this
   Async.background {
        // because `Driver`  
        xsDriver.driveNext { entities in
             print("E2 \(entities)")
        }

        // first print would be called on background thread because
        // it will be replayed immediately from `shareReplay` operator, 
        // and subsequent elements would be printed on main scheduler because 
        // they would pass through `observeOn(MainScheduler.instance)` operator.

        // that assert is here to warn you that it is possible that first element emitting from 
        // `shareReplay` operator would happen on background thread  

        // if you always call `drive` method from main thread, then this can't happen since 
        // even that first element will be replayed on main thread
    }

This is the rationale:

  • working with UI elements and other elements that should be only used from main thread, even touching/referencing/retaining them from background schedulers is not allowed as far as I can tell. Just this code itself is invalid because it touches UIImageView reference from background thread.
    Async.background {
      print(self.avatar) // retains UIView from background thread which isn't allowed 
                                 // as far as I'm aware of
    }
  • adding 2nd observeOn operator would schedule binding on 2nd run loop pass, and thus potentially cause UI glitches.
  • I don't see any reason why somebody should be able to call drive method from background thread since this is a concept intended to model UI data streams.

All 3 comments

Ok, maybe I went too fast, just saw this https://github.com/ReactiveX/RxSwift/issues/743

So, the only way to warranty that a binding is done in the MainThread (independently of the thread your are coming from) is explicitly using observeOn(MainScheduler.instance) (that is not available in Driver)? In that case and for this scenario, I do not see the point of using drivers, because anyway I would need to do:

driver.asObservable().observeOn(MainScheduler.instance).bindTo(self.avatar.rx_image).addDisposableTo(self.disposeBag)

Hi @acecilia ,

You need to call drive* from main thread. Everything documented is true. Driver does indeed have observeOn(MainScheduler.instance) internally to ensure elements are received on main thread.

It is easy to verify that source code for ObservableType.asDriver* methods contains observeOn(MainScheduler.instance) to make sure all event are emitted on main scheduler.

The reason why you need to also call drive method from main thread is because it also has shareReplay operator that is used as a sharing strategy that replays latest element immediately upon subscription on the same thread which is calling drive* method.

This is the scenario:

let xs: Observable<[DatabaseEntity]> = ....
let xsDriver: Driver<[DataBaseEntity]> = xs.asDriver(onErrorJustReturn: [])

// this is on main thread
xsDriver.driveNext { entities in
    print("E1 \(entities)")
}

// prints E1 [....

// so now somebody does this
   Async.background {
        // because `Driver`  
        xsDriver.driveNext { entities in
             print("E2 \(entities)")
        }

        // first print would be called on background thread because
        // it will be replayed immediately from `shareReplay` operator, 
        // and subsequent elements would be printed on main scheduler because 
        // they would pass through `observeOn(MainScheduler.instance)` operator.

        // that assert is here to warn you that it is possible that first element emitting from 
        // `shareReplay` operator would happen on background thread  

        // if you always call `drive` method from main thread, then this can't happen since 
        // even that first element will be replayed on main thread
    }

This is the rationale:

  • working with UI elements and other elements that should be only used from main thread, even touching/referencing/retaining them from background schedulers is not allowed as far as I can tell. Just this code itself is invalid because it touches UIImageView reference from background thread.
    Async.background {
      print(self.avatar) // retains UIView from background thread which isn't allowed 
                                 // as far as I'm aware of
    }
  • adding 2nd observeOn operator would schedule binding on 2nd run loop pass, and thus potentially cause UI glitches.
  • I don't see any reason why somebody should be able to call drive method from background thread since this is a concept intended to model UI data streams.

Nice @kzaher, thank you so much for that great explanation, I appreciate it.

Was this page helpful?
0 / 5 - 0 ratings