How to conditionally observe and bind with RxSwift - ios

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)

Related

Swift Combine two handlers for one property

I have a UIViewController which using UITableViewDiffableDataSource. I have a view-model for this controller which looks something like:
class ListViewModel {
#Published private(set) var items: [Item] = []
func load(params: [String: Any] = [:]) {
WebRepo().index(params: params, completion: { [weak self] (items, error) in
self?.items = items
})
}
func deleteFirst() {
self.items.remove(object: self.items.first)
}
}
In my VC, I have a binding like:
self.viewModel.$items.sink { [weak self] (scenes) in
self?.update(items: items, animated: false)
}.store(in: &self.subscriptions)
So, when I'm calling my view-model's load method - I want to do self?.update(items: items, animated: false), but when I'm calling deleteFirst - I want self?.update(items: items, animated: true).
I'm quite new to reactive and Combine, so not sure what is the proper way to handle this.
I can add isReset property to my view-model and change load method to something like:
func load(params: [String: Any] = [:]) {
WebRepo().index(params: params, completion: { [weak self] (items, error) in
self?.isReset = true
self?.items = items
self?.isReset = false
})
}
And inside sink just check this property, but it does not look as a proper way for me.
Here's one way of thinking about it. Instead of publishing items, use a PassthroughSubject whose output type is a tuple, ([Items], Bool). And the view controller subscribes to that subject.
Now, when you call load, call the passthrough subject's send with (items, false), but when you call delete, call the passthrough subject's send with (items, true).
In other words, take it upon your publisher to publish all the information that the downstream would need in order to know what to do.
You might think that this approach is rather extreme, but clumping things together into a tuple in order to pass multiple pieces of info down the pipeline is normal behavior. Really, this is the reactive equivalent of calling a method with two parameters.
Another possibility might be for the downstream to consider how many times this publisher has published. This would work if, for example, the ViewModel is going to call load only once. If that's the case, the downstream pipeline would be able to use operators (such as scan, or first, or whatever) to distinguish the first value that comes down the pipeline (which means we want not to animate) from any subsequent values (where we do want to animate).
Yet another way to think of this would be to put the onus entirely on whoever is building the snapshot. If the diffable data source's snapshot is empty, it has no data and we do not want to animate. If is not empty, we do want to animate. Again, that would work only if applicable to your purposes.

Collecting stored variable property using withLatestFrom

I'm wondering if there is a way in RxSwift to observe value of stored variable property. Eg. in following example:
var updatedValue: Int = 0
var observedValue: Observable<Int> {
return Observable.create({ (observer) -> Disposable in
observer.onNext(updatedValue)
return Disposables.create()
})
}
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
updatedValue = updatedValue + 1;
}
let myObservable = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.publish()
myObservable.connect()
myObservable
.withLatestFrom(observedValue)
.subscribe { (event) in
print(event)
}
We have variable property updatedValue and hot observable myObservable. We also increment value of updatedValue in Timer.scheduledTimer....
Flow here is pretty straight forward. When we subscribe, observedValue gets called, we get onNext from observedValue and then Disposables.create(). Then we print event onNext(0).
As myObservable is based on Observable.interval, same withLatestFrom value gets printed in onNext every second.
Question: Is there a way to print last value of updatedValue every time myObservable emits new event? So instead of 0,0,0... we get 0,1,2...
I'm aware that updatedValue could be declared as BehaviorRelay.
I'm also aware that we could use .map { } to capture self.updatedValue.
But I'm wondering if there is any way to create a Observable wrapper around standard variable property so it calls onNext with most recent value every time trigger sequence sends an event? Without capturing self or changing declaration on updatedValue.
Thanks for any comments and ideas!
RxCocoa has a handy wrapper around KVO. You should be able to use it from .rx extension on NSObject subclasses.
For your issue, I guess you can do something like:
let updatedValueObservable = self.rx.observe(Int.self, "updatedValue")
But I'm wondering if there is any way to create a Observable wrapper around standard variable property so it calls onNext with most recent value every time trigger sequence sends an event? Without capturing self or changing declaration on updatedValue.
The correct answer is, no. There is no way to do anything to updatedValue without involving self. One way of doing it would be with Observable<Int>.interval(1, scheduler: MainScheduler.instance).compactMap { [weak self] _ in self?.updatedValue }.distinctUntilChanged() (Your use of publish and connect is odd and unnecessary,) but that involves self.
Since your property is a value type, the only way to access it is through self, even if Rx wasn't involved at all.

RxSwift observe array change only when appended new element

Is there a way to subscribe to array change only when a new element is appended?
So I want the following closure to be executed only when a new element is appended.
array.asObservable().subscribe(onNext: { array in
}).disposed(by: self.bag)
If, for example, an element is removed from this array, I don't want this closure to be executed.
EDIT:
Is there a way to only have newly appended elements in the closure? In my case I append subsequences of various lengths, so I can't just look at the last element of the array inside the closure.
Maybe something like this
let array = Variable<[Int]>([])
array.asObservable().distinctUntilChanged { $0.count > $1.count}.subscribe(onNext: {
print($0)
}).disposed(by: disposeBag)
array.value.append(1) //called
array.value.append(2) //called
array.value.remove(at: 0) //not called
array.value.append(3) //called
In that case you should resign using Variable and use Subjects. I think, that your requirements should meet PublishSubject:
let subjectArray = PublishSubject<[Int]>([])
array.asObservable().subscribe(onNext: {
print($0)
}).disposed(by: disposeBag)
When you add new elements to subjectArray only that new elements will be printed.

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.

How do I initialize an observable property in RxSwift?

I find this very puzzling. Coming from ReactiveCocoa I would expect something like this possible.
How can I initialize the RxSwift observable to 5?
You can create stream in multiple ways:
Main way
Observable<Int>.create { observer -> Disposable in
// Send events through observer
observer.onNext(3)
observer.onError(NSError(domain: "", code: 1, userInfo: nil))
observer.onCompleted()
return Disposables.create {
// Clean up when stream is deallocated
}
}
Shortcuts
Observable<Int>.empty() // Completes straight away
Observable<Int>.never() // Never gets any event into the stream, waits forever
Observable<Int>.just(1) // Emit value "1" and completes
Through Subjects (aka Property / MutableProperty in ReactiveSwift)
Variable is deprecated in RxSwift 4, but it's just a wrapper around BehaviourSubject, so you can use it instead.
There are 2 most used subjects
BehaviorSubject - it will emit current value and upcoming ones. Because it will emit current value it needs to be initialised with a value BehaviorSubject<Int>(value: 0)
PublishSubject - it will emit upcoming values. It doesn't require initial value while initialising PublishSubject<Int>()
Then you can call .asObservable() on subject instance to get an observable.
In RxSwift Variable is deprecated. Use BehaviorRelay or BehaviorSubject
I am not able to test it right now, but wouldn't Observable.just be the function you are looking for?
Source for Observable creation: github link
Of course, you could also use a Variable(5) if you intend to modify it.
As I am learning RxSwift I ran up on this thread. You can initialize an observable property like this:
var age = Variable<Int>(5)
And set it up as an observable:
let disposeBag = DisposeBag()
private func setupAgeObserver() {
age.asObservable()
.subscribe(onNext: {
years in
print ("age is \(years)")
// do something
})
.addDisposableTo(disposeBag)
}
in Rx in general, there is a BehaviorSubject which stores the last value
specifically in RxSwift, there is also Variable which is a wrapper around BehaviorSubject
see description of both here - https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md

Resources