RxSwift - Why wrap ControlProperty trait into a Driver? - ios

In the official RxSwift documentation, it's described that traits Driver and ControlProperty share many similarities between them (can't error out, observe occurs on main scheduler, share and replay side effects), but at the same time in the example provided ControlProperty rx.text is being wrapped into a Driver.
So the questions would be:
Is there any real advantage of wrapping a ControlProperty into a Driver trait?
If both ControlProperty and Driver are supposed to share and replay by default, why is .share(replay: 1) operator being called in the first code but not in the second?
Here I attach the referenced code from the documentation:
From:
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // results are returned on MainScheduler
.catchErrorJustReturn([]) // in the worst case, errors are handled
}
.share(replay: 1) // HTTP requests are shared and results replayed
// to all UI elements
results
.map { "\($0.count)" }
.bind(to: resultCount.rx.text)
.disposed(by: disposeBag)
results
.bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)
To:
let results = query.rx.text.asDriver() // This converts a normal sequence into a `Driver` sequence.
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // Builder just needs info about what to return in case of error.
}
results
.map { "\($0.count)" }
.drive(resultCount.rx.text) // If there is a `drive` method available instead of `bind(to:)`,
.disposed(by: disposeBag) // that means that the compiler has proven that all properties
// are satisfied.
results
.drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.disposed(by: disposeBag)
Thanks and best regards!

In the first example, the throttle that is used returns an Observable.
In the second example, because of the asDriver() call, a different throttle is used that returns a Driver (i.e., a SharedSequence<DriverSharingStrategy, String>)
Your confusion likely stems from the fact that there are two throttle functions in the RxSwift library. One is an extension on ObservableType (ControlProperty is an extension of ObservableType) whereas the other is an extension on SharedSequenceConvertibleType (Driver is an extension of that.)

Related

Swift Combine operator with same functionality like `withLatestFrom` in the RxSwift Framework

I'm working on an iOS application adopting the MVVM pattern, using SwiftUI for designing the Views and Swift Combine in order to glue together my Views with their respective ViewModels.
In one of my ViewModels I've created a Publisher (type Void) for a button press and another one for the content of a TextField (type String).
I want to be able to combine both Publishers within my ViewModel in a way that the combined Publisher only emits events when the button Publisher emits an event while taking the latest event from the String publisher, so I can do some kind of evaluation on the TextField data, every time the user pressed the button. So my VM looks like this:
import Combine
import Foundation
public class MyViewModel: ObservableObject {
#Published var textFieldContent: String? = nil
#Published var buttonPressed: ()
init() {
// Combine `$textFieldContent` and `$buttonPressed` for evaulation of textFieldContent upon every button press...
}
}
Both publishers are being pupulated with data by SwiftUI, so i will omit that part and let's just assume both publishers receive some data over time.
Coming from the RxSwift Framework, my goto solution would have been the withLatestFrom operator to combine both observables.
Diving into the Apple Documentation of Publisher in the section "Combining Elements from Multiple Publishers" however, I cannot find something similar, so I expect this kind of operator to be missing currently.
So my question: Is it possible to use the existing operator-API of the Combine Framework to get the same behavior in the end like withLatestFrom?
It sounds great to have a built-in operator for this, but you can construct the same behavior out of the operators you've got, and if this is something you do often, it's easy to make a custom operator out of existing operators.
The idea in this situation would be to use combineLatest along with an operator such as removeDuplicates that prevents a value from passing down the pipeline unless the button has emitted a new value. For example (this is just a test in the playground):
var storage = Set<AnyCancellable>()
var button = PassthroughSubject<Void, Never>()
func pressTheButton() { button.send() }
var text = PassthroughSubject<String, Never>()
var textValue = ""
let letters = (97...122).map({String(UnicodeScalar($0))})
func typeSomeText() { textValue += letters.randomElement()!; text.send(textValue)}
button.map {_ in Date()}.combineLatest(text)
.removeDuplicates {
$0.0 == $1.0
}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
typeSomeText()
typeSomeText()
typeSomeText()
pressTheButton()
typeSomeText()
typeSomeText()
pressTheButton()
The output is two random strings such as "zed" and "zedaf". The point is that text is being sent down the pipeline every time we call typeSomeText, but we don't receive the text at the end of the pipeline unless we call pressTheButton.
That seems to be the sort of thing you're after.
You'll notice that I'm completely ignoring what the value sent by the button is. (In my example it's just a void anyway.) If that value is important, then change the initial map to include that value as part of a tuple, and strip out the Date part of the tuple afterward:
button.map {value in (value:value, date:Date())}.combineLatest(text)
.removeDuplicates {
$0.0.date == $1.0.date
}
.map {($0.value, $1)}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
The point here is that what arrives after the line .map {($0.value, $1)} is exactly like what withLatestFrom would produce: a tuple of both publishers' most recent values.
As improvement of #matt answer this is more convenient withLatestFrom, that fires on same event in original stream
Updated: Fix issue with combineLatest in iOS versions prior to 14.5
extension Publisher {
func withLatestFrom<P>(
_ other: P
) -> AnyPublisher<(Self.Output, P.Output), Failure> where P: Publisher, Self.Failure == P.Failure {
let other = other
// Note: Do not use `.map(Optional.some)` and `.prepend(nil)`.
// There is a bug in iOS versions prior 14.5 in `.combineLatest`. If P.Output itself is Optional.
// In this case prepended `Optional.some(nil)` will become just `nil` after `combineLatest`.
.map { (value: $0, ()) }
.prepend((value: nil, ()))
return map { (value: $0, token: UUID()) }
.combineLatest(other)
.removeDuplicates(by: { (old, new) in
let lhs = old.0, rhs = new.0
return lhs.token == rhs.token
})
.map { ($0.value, $1.value) }
.compactMap { (left, right) in
right.map { (left, $0) }
}
.eraseToAnyPublisher()
}
}
Kind-of a non-answer, but you could do this instead:
buttonTapped.sink { [unowned self] in
print(textFieldContent)
}
This code is fairly obvious, no need to know what withLatestFrom means, albeit has the problem of having to capture self.
I wonder if this is the reason Apple engineers didn't add withLatestFrom to the core Combine framework.

RXSwift Not subscribing on Main Thread

I am trying to make several API calls and populate a Realm Database.
Everything works fine. However when I try to run performSegue() on subscribe() method an exception is raised, informing that I can't do this on a background thread, which is perfectly reasonable.
But since I am subscribing to MainScheduler.instance shouldn't the subscribe() method run on UI Thread?
Single.zip(APIClient.shared.getSchools(), APIClient.shared.getPointsOfInterest())
.observeOn(SerialDispatchQueueScheduler(qos: .background))
.flatMap { zip in return Single.zip(SchoolDao.shared.insertSchools(schoolsJson: zip.0), PointOfInterestDao.shared.insertPointsOfInterest(poisJson: zip.1))}
.flatMap{ _ in Single.zip(SchoolDao.shared.countSchools(), PointOfInterestDao.shared.countPointsOfInterest())}
.subscribeOn(MainScheduler.instance)
.subscribe(onSuccess: { tableCounts in
let (schoolsCount, poisCount) = tableCounts
if(schoolsCount != 0 && poisCount != 0){
print(Thread.isMainThread) //Prints False
self.performSegue(withIdentifier: "splashToLogin", sender: nil)
}
}, onError: {
error in return
}).disposed(by: disposeBag)
Am I making a wrong assumption on how does RXSwift works?
Edit: If I add this line .observeOn(MainScheduler.instance) after .subscribeOn(MainScheduler.instance) the subscribe method runs on Main thread. Is this correct behavior? What is .subscribeOn(MainScheduler.instance) even doing?
Your edit explains all. Your initial assumption on what subscribeOn and observeOn were backwards.
The subscribeOn operator refers to how the observable above the operator in the chain subscribes to the source of events (and likely doesn't do what you think it does in any case. Your two network calls likely set up their own background thread to emit values on regardless of how they are subscribed to.)
For example, look at this:
extension ObservableType {
func subscribeOnMain() -> Observable<Element> {
Observable.create { observer in
let disposable = SingleAssignmentDisposable()
DispatchQueue.main.async {
disposable.setDisposable(self.subscribe(observer))
}
return disposable
}
}
}
It makes it obvious why the operator is called subscribeOn. It's because the subscribe is happening on the scheduler/thread in question. And this helps you understand better what is happening when you stack subscribeOn operators...
The observeOn operator refers to the scheduler that will be emitting elements to the observer (which is the block(s) of code that are passed to the subscribe operator.)
Which would look like this:
extension ObservableType {
func observeOnMain() -> Observable<Element> {
Observable.create { observer in
self.subscribe { event in
DispatchQueue.main.async {
observer.on(event)
}
}
}
}
}
From this you can see that the subscribe is happening on the original scheduler, while the observer is being called on the new scheduler.
Here is a great article explaining the whole thing: http://rx-marin.com/post/observeon-vs-subscribeon/

RxSwift: Why does flatMapLatest never execute onCompleted()?

In my Swift UIViewController, I'm attempting to subscribe to a class member of type Variable, run it through a flatMapLatest call, and then have the onCompleted() call in the flatMapLatest observable execute on all subscribers. However, while onNext() is called, onCompleted() never is and I'm not sure why.
My class member is defined as:
private let privateVar = Variable<String>("")
while in my viewDidLoad() method, I set up the observables:
let localVar = self.privateVar.asObservable().distinctUntilChanged()
localVar.subscribe(onNext: { [weak self] sent in print("first onNext called") })
.disposed(by: self.disposeBag)
let mappedVar = localVar.flatMapLatest { self.ajaxLoad(var1: $0) }.share()
mappedVar.subscribe(
onNext: { [weak self] queryRes in
print("onNext called!")
},
onCompleted: { [weak self] in
print("onCompleted called!")
}
)
.disposed(by: self.disposeBag)
and my ajaxLoad method:
func ajaxLoad(var1 myVar: String) -> Observable<QueryResponse> {
return Observable.create { observable in
apollo.fetch(query: MyQuery()) { (result, _) in
observable.onNext(result?.data?.myQuery)
observable.onCompleted()
}
return Disposables.create()
}
}
I'm fairly new to ReactiveX so I may be a little hazy on what the Rx lifecycle actually looks like. Why might onNext be called in the flatMapLatest call, but not onCompleted? Any help would be appreciated. Thanks in advance!
The flatMap operator does not emit completed events of any observable that you return inside the block.
The following code illustrates this clearly. .just(_) emits the element and then a completed event, which does not terminate to subscription.
_ = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.debug("before flatmap")
.flatMap { .just($0 * 2) }
.debug("after flatmap")
.subscribe()
In fact, Variable only emits completed when deallocated. See source v4.0.
Note that Variable is deprecated in RxSwift 4, you are encouraged to use RxCocoa's similar BehaviorRelay instead.
deinit {
_subject.on(.completed)
}
Since you said you are new and a "little hazy"...
Keep in mind that whenever localVar changes, it emits a new value and ajaxLoad(var1:) gets called. The result of ajaxLoad(var1:) then gets pushed to your subscribe's onNext closure.
Also keep in mind that if an Observable emits a .completed it's dead. It can no longer emit anything else.
So flatMapLatest can't complete (unless its source completes.) If it did, it would kill the whole pipe and no more changes to localVar would get routed through the pipe, ajaxLoad(var:1) wouldn't get called again with the new value and nothing more would get pushed to the subscribe's onNext method.
A sequence of observables can be thought of like a Rube Goldberg machine where a completed shuts down the machine and an error breaks it. The only time you should shut down the machine is if the source (in this case localVar) is finished emitting values or the sink (destination, in this case the onNext: closure) doesn't want any more values.

How to conditionally observe and bind with RxSwift

I'm trying to observe a Variable and when some property of this variable fits a condition, I want to make an "observable" API call and bind the results of that call with some UI element. It is working the way I present it here, but I'm having the thought that it could be implemented way better, because now I'm nesting the subscription methods:
self.viewModel.product
.asObservable()
.subscribe { [weak self](refreshProduct) in
self?.tableView.reloadData()
self?.marketProduct.value.marketProduct = refreshProduct.element?.productId
if refreshProduct.element?.stockQuantity != nil {
self?.viewModel.getUserMarketCart()
.map({ (carts) -> Bool in
return carts.cartLines.count > 0
}).bind(onNext: { [weak self](isIncluded) in
self?.footerView.set(buyable: isIncluded)
}).disposed(by: (self?.disposeBag)!)
}
}.disposed(by: disposeBag)
Is there any other way to do this? I can get a filter on the first observable, but I don't understand how I can call the other one and bind it on the UI.
NOTE: I excluded a few other lines of code for code clarity.
A typical solution would be using .switchLatest() as follows.
create *let switchSubject = PublishSubject<Observable<Response>>()"
bind UI to it's latest value: switchSubject.switchLatest().bind(...
update 'switchSubject' with new requests: switchSubject.onNext(newServiceCall)

Chaining RxSwift observable with different type

I need request different types of models from network and then combine them into one model.
How is it possible to chain multiple observables and return another observable?
I have something like:
func fetchDevices() -> Observable<DataResponse<[DeviceModel]>>
func fetchRooms() -> Observable<DataResponse<[RoomModel]>>
func fetchSections() -> Observable<DataResponse<[SectionModel]>>
and I need to do something like:
func fetchAll() -> Observable<(AllModels, Error)> {
fetchSections()
// Then if sections is ok I need to fetch rooms
fetchRooms()
// Then - fetch devices
fetchDevices()
// And if everything is ok create AllModels class and return it
// Or return error if any request fails
return AllModels(sections: sections, rooms: rooms, devices:devices)
}
How to achieve it with RxSwift? I read docs and examples but understand how to chain observables with same type
Try combineLatest operator. You can combine multiple observables:
let data = Observable.combineLatest(fetchDevices, fetchRooms, fetchSections)
{ devices, rooms, sections in
return AllModels(sections: sections, rooms: rooms, devices:devices)
}
.distinctUntilChanged()
.shareReplay(1)
And then, you subscribe to it:
data.subscribe(onNext: {models in
// do something with your AllModels object
})
.disposed(by: bag)
I think the methods that fetching models should reside in ViewModel, and an event should be waiting for start calling them altogether, or they won't start running.
Assume that there's a button calls your three methods, and one more button that will be enabled if the function call is succeeded.
Consider an ViewModel inside your ViewController.
let viewModel = ViewModel()
In ViewModel, declare your abstracted I/O event like this,
struct Input {
buttonTap: Driver<Void>
}
struct Output {
canProcessNext: Driver<Bool>
}
Then you can clearly transform your Input into Output by making function like this in ViewModel.
func transform(input: Input) -> Output {
// TODO: transform your button tap event into fetch result.
}
At viewDidLoad,
let output = viewModel.transform(input: yourButton.rx.tap.asDriver())
output.drive(nextButton.rx.isEnabled).disposed(by: disposeBag)
Now everything's ready but combining your three methods - put them in ViewModel.
func fetchDevices() -> Observable<DataResponse<[DeviceModel]>>
func fetchRooms() -> Observable<DataResponse<[RoomModel]>>
func fetchSections() -> Observable<DataResponse<[SectionModel]>>
Let's finish the 'TODO'
let result = input.buttonTap.withLatestFrom(
Observable.combineLatest(fetchDevices(), fetchRooms(), fetchSections()) { devices, rooms, sections in
// do your job with response data and refine final result to continue
return result
}.asDriver(onErrorJustReturn: true))
return Output(canProcessNext: result)
I'm not only writing about just make it work, but also considering whole design for your application. Putting everything inside ViewController is not a way to go, especially using Rx design. I think it's a good choice to dividing VC & ViewModel login for future maintenance. Take a look for this sample, I think it might help you.

Resources