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:
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!
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
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!
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 toDriver, you can just do:Another thing is that you are converting to driver too late:
You would probably want to do:
to shorten this, you can create
Driverextensionand use it