RxSwift: FlatMap on nested observables - reactive-programming

I have this editor view model that I use in different other view models. The parent view models have a selectable user, once a user is selected, I'm gonna need a new instance of the editor with the new user.
This is a simplified version of the editor and a parent.
class EditorViewModel {
let user: String
let item = PublishSubject<String>()
init(user: String) {
self.user = user
}
}
class ParentViewModel {
var editor: Observable<EditorViewModel>!
let user = BehaviorSubject<String?>(value: nil)
init() {
editor = user.compactMap { $0 }.map { EditorViewModel(user: $0) }
}
}
Once the editor saves an item, I expect to get the saved item by flatMaping the editor to its item. Like this:
let parent = ParentViewModel()
parent.editor.flatMapLatest { $0.item }.debug("item").subscribe(onNext: { item in
print("This doesn't print")
})
parent.editor.subscribe(onNext: { editor in
print("This one prints")
editor.item.onNext("something")
})
parent.user.onNext("1")
The flatMap line does subscribe but it never gets an item.
This is the output for running the code above in the playground:
2021-10-28 13:47:41.528: item -> subscribed
This one prints
Also, if you think this is too crazy a setup, I concur and am open to suggestions.

By default, Observables are cold. This means that each subscription works independently and in this case each subscription is getting a different EditorViewModel. (The .map { EditorViewModel(user: $0) } Observable will call its closure for each subscription.)
Adding a .share() or .share(replay: 1) after the .map { EditorViewModel(user: $0) } Observable will make it hot which means that all subscriptions will share the same effect.
As to your sub-question. I don't think I would setup such a system unless something outside of this code forced me to. Instead, I would pass an Observable into the EditorViewModel instead of a raw User. That way you don't need to rebuild editor view models every time the user changes.

Related

How to make a proper reactive extension on Eureka SelectableSection

This is my first question to the StackOverflow community so excuse me if I'm doing something wrong.
1. What I'm trying to achieve
Basically, I want to make a custom reactive wrapper around Eureka's SelectableSection class in order to observe the value of the selected row when it is changed. I'm thinking to get this data from the onSelectSelectableRow closure which is called every time a row is selected.
2. What I've tried to do for that
Actually, I've got this working but it's not a generic use of the custom wrapper, here is the example that works but only when I specify the row and its value type, for example ListCheckRow<Int>.
extension SelectableSection: ReactiveCompatible {}
extension Reactive where Base : SelectableSection<ListCheckRow<Int>> {
var selectedValue: Observable<Base.SelectableRow.Cell.Value?> {
return Observable.create { observer in
self.base.onSelectSelectableRow = {cell, row in
observer.onNext(row.value)
}
return Disposables.create {
observer.onCompleted()
}
}
}
}
This works fine and as I expected but when it comes to something more generic like the next code example, I get an error saying that: "Cannot assign to property: 'base' is a 'let' constant"
extension SelectableSection: ReactiveCompatible {}
extension Reactive where Base : SelectableSectionType {
var selectedValue: Observable<Base.SelectableRow.Cell.Value?> {
return Observable.create { observer in
self.base.onSelectSelectableRow = {cell, row in // Error: Cannot assign to property: 'base' is a 'let' constant
observer.onNext(row.value)
}
return Disposables.create {
observer.onCompleted()
}
}
}
}
Any help will be much appreciated, thanks. 🙏
The fundamental problem here is that SelectableSectionType is a protocol that isn't restricted to class types and Reactive assumes that Base is a class (or otherwise is not going to be modified by the observable creation.)
I think the most generic you can make this is something like:
extension Reactive {
func selectedValue<Row, T>() -> Observable<T?> where Base: SelectableSection<Row>, Row: SelectableRowType, T == Row.Cell.Value {
Observable.create { [base] observer in
base.onSelectSelectableRow = { cell, row in
observer.onNext(row.value) // this is problematic. See below.
}
return Disposables.create {
observer.onCompleted() // this is wrong. See below.
}
}
}
}
The biggest problem with the above though is that if you subscribe to the resulting Observable more than once or create more than one Observable using this computed property, all but the last subscription will silently fail. The simple way to fix this is to always remember to share any result but that's rather error prone.
The way to fix this would be to associate a Subject with each SelectableSection, but you can't modify the class, so what are we to do?
Here's a solution:
extension Reactive {
func selectedValue<Row, T>() -> Observable<T?> where Base: SelectableSection<Row>, Row: SelectableRowType, T == Row.Cell.Value {
Observable.create { [base] observer in
if let block = selectableSections.first(where: { $0.section === base }) {
let subject = block.subject as! PublishSubject<T?>
return Disposables.create(
block.disposable.retain(),
subject.subscribe(observer)
)
}
else {
let subject = PublishSubject<T?>()
let block = SelectableSectionBlock(
section: base,
subject: subject,
disposable: RefCountDisposable(disposable: Disposables.create {
selectableSections.removeAll(where: { $0.section === base })
})
)
base.onSelectSelectableRow = { cell, row in
subject.onNext(row.value)
}
selectableSections.append(block)
return Disposables.create(
block.disposable,
subject.subscribe(observer)
)
}
}
}
}
private struct SelectableSectionBlock {
let section: Section
let subject: Any
let disposable: RefCountDisposable
}
private var selectableSections = [SelectableSectionBlock]()
The selectableSections array stores a Subject and RefCountDisposable for each SelectableSection.
Whenever an Observable is created, or subscribed to...
if it's the first time working with this section, it will create a Subject and RefCountDisposable assign the onSelectSelectableRow to send a next event to the subject and store the subject in the array.
otherwise it will find the subject and disposable associated with this Section and retain the disposable.
Once it has the subject and disposable from above, it will subscribe the new observer to the subject and return a new Disposable that will remove that subscription and decrement the ref-count when the time comes.
Yes this is quite a bit more complex than the simple assignment case, but it's the right thing to do.
As for calling onCompleted() inside the disposable closure. By the time the closure is called, the observer has already emitted an onCompleted/onError event, or the observer has stopped listening to the observable. So this event will never be seen.

Update menu from within RxSwift subscribe

I am updating a menu (adding, deleting item) from within a RxSwift subscriber. This is how the menu supposed to look like:
The "Item A" will be continuously added and removed, depending on changes of the model, like the following:
// Using ObservableArray (https://github.com/safx/ObservableArray-RxSwift)
model.changeset.rx()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (changes) in
// Inserts
for change in changes.insertedElements {
guard let item = self.newItem(item: change) else { continue }
let index = (self.view?.indexPlaceholder(at: .specialItem) ?? 0) + 1
// self.view is a NSMenu Object, so menu item will be added here
self.view?.insertItem(bridgeItem, at: index)
}
// Deletes
for change in changes.deletedElements {
guard let menuItems = self.view?.getItems(withIdentifier: .specialItem) else { continue }
guard let newIndex = menuBridgeObjects.firstIndex(where: {
...
}
let index = (self.view?.indexPlaceholder(at: .specialItem) ?? 0) + 1 + index
// self.view is a NSMenu Object, so menu item will be removed here
self.view?.removeItem(safe: index, onlyIf: .specialItem)
}
}).disposed(by: disposeBag)
}
The model.changeset will be populated or altered based on several network activities running in the background.
However, I have realized that while the menu is open, any modifications to the model.changeset and the menu changes through the subscriber, the menu looks like this (missing the separator item below "Item A"):
After closing the menu (tracking lost) and re-open again (no changes on the model this time, so code snippet above will not be triggered), the menu looks as it was supposed to be like this:
I already tried something like NSMenu.update(), but this is somehow not helping to draw the NSMenu properly while open. Do you know if I have overseen something very important here?
The code above will be enabled and triggered immediately after NSMenu's delegate func menuWillOpen(_ menu: NSMenu)
The above feels wrong to me. You should instead setup the code to trigger any time the array changes, don't tie it to menuWillOpen(_:). That way the menu items array will always be in the correct state when the menu opens.

Subscribe to view controller property without nested subscribe loop

How can I make this subscribe loop not be nested? I can't seem to figure out how you would go about doing this because I push the view controller in the main subscribe loop, and not just set a value.
button.rx.tap.subscribe(onNext: { _ in
let viewController = MyViewController()
self.navigationController.pushViewController(viewController)
viewController.myPublishRelay.asObservable().subscribe(onNext: { value in
// do something with value
})
})
You desire two different side effects, so it makes sense to have two subscription. To prevent from nesting, you could do something in the lines of this:
let viewControllerToPresent = button.rx.tap.map { _ in
return MyViewController()
}.share()
viewControllerToPresent.subscribe(onNext: { [unowned self] viewController in
self.view.pushViewController(viewController)
}.disposed(by: self.disposeBag)
viewControllerToPresent.flatMapLatest { viewController in
viewController.myPublishRelay.asObservable()
}.subscribe(onNext: { value in
// do something with value
}.disposed(by: self.disposeBag)
The call to share is important here, otherwise the mapping after rx.tap would occur twice, making us subscribe to the publish relay of a view controller that is not the one we presented.
You can use .sample() or .combineLatest(), depending on how does your publishRelay update.
For example, Observable.combineLatest(myPublishRelay, button.rx.tap) { $0 }.subscribe(onNext: { value ...
See http://rxmarbles.com for reference on operators.
Whenever I see a nested subscribe I think of flatMap. Something like this should work:
button.rx.tap
.flatMap { _ in
let viewController = MyViewController()
self.navigationController.pushViewController(viewController)
return viewController.myPublishRelay.asObservable()
}
.subscribe(onNext: { value in
// do something with value
})

Observable being disposed ahead of time

I think it's better if I explain what I'm trying to achieve because I think the error is on my misunderstanding on how Observables work.
I have a UIViewController that contains a UITableView I'm also using RxSwift and RxDataSources, so I'm binding my tableView items like this:
vm.model
.debug()
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
Where vm is a viewModel which contains:
self.model = self.network.provider.getMarkets()
.map { (markets: [Market]) -> [Row] in
var rows = [Row]()
for market in markets {
rows.append(.market(market: market))
}
return rows
}
.map { (rows: [Row]) -> [Model] in
return [Model(section: .market, items: rows)]
}
.shareReplay(1)
.asDriver(onErrorJustReturn: [])
Where model is:
var model: Driver<[Model]>
This all works great the first time, the tableview displays the items, but the print from the debug():
2017-04-28 20:07:21.382: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> subscribed
2017-04-28 20:07:22.287: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> Event next(*Multiple items*)
2017-04-28 20:07:22.289: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> Event completed
2017-04-28 20:07:22.289: MarketAndLanguageSelectionViewController.swift:36 (viewDidLoad()) -> isDisposed
The problem is I didn't want the datasource to dispose because I wan't to update it based on the user action. If the user clicks a tableViewCell I want to update the model. Any ideas on how can I achieve this?
Sorry for such a big question.
I'm guessing that network.provider.getMarkets() makes a network call which returns a single result and completes.
Now, getMarkets() is the source, and tableView.rx.items is the sink. Once the source completes, the chain is broken.
It sounds like what you want to do is create a new getMarkets Observable every time the user taps something, as well as calling getMarkets once for free. I would expect something like:
let markets = trigger.flatMap {
self.network.provider.getMarkets()
}.map { (markets: [Market]) -> [Row] in
var rows = [Row]()
for market in markets {
rows.append(.market(market: market))
}
return rows
}.map { (rows: [Row]) -> [Model] in
return [Model(section: .market, items: rows)]
}.startWith(self.network.provider.getMarkets())
.shareReplay(1)
.asDriver(onErrorJustReturn: [])
Note that the only real difference is the beginning trigger.flatMap {. Your source will then be the button or whatever the user taps on to cause the network update which won't complete until it's deleted.
(The above is untested code, but it should give you an idea of the shape you want.)

RxSwift: Nested Queries and ReplaySubject

I have to fetch three types of data (AType, BType, CType) using three separate API requests. The objects returned by the APIs are related by one-to-many:
1 AType object is parent of N BType objects
1 BType object is parent of P CType objects)
I'm using the following three functions to fetch each type:
func get_A_objects() -> Observable<AType> { /* code here */ }
func get_B_objects(a_parentid:Int) -> Observable<BType> { /* code here */}
func get_C_objects(b_parentid:Int) -> Observable<CType> { /* code here */}
and to avoid nested subscriptions, these three functions are chained using flatMap:
func getAll() -> Observable<CType> {
return self.get_A_objects()
.flatMap { (aa:AType) in return get_B_objects(aa.id) }
.flatMap { (bb:BType) in return get_C_objects(bb.id) }
}
func setup() {
self.getAll().subscribeNext { _ in
print ("One more item fetched")
}
}
The above code works fine, when there are M objects of AType, I could see the text "One more item fetched" printed MxNxP times.
I'd like to setup the getAll() function to deliver status updates throughout the chain using ReplaySubject<String>. My initial thought is to write something like:
func getAll() -> ReplaySubject<String> {
let msg = ReplaySubject<String>.createUnbounded()
self.get_A_objects().doOnNext { aobj in msg.onNext ("Fetching A \(aobj)") }
.flatMap { (aa:AType) in
return get_B_objects(aa.id).doOnNext { bobj in msg.onNext ("Fetching B \(bobj)") }
}
.flatMap { (bb:BType) in
return get_C_objects(bb.id).doOnNext { cobj in msg.onNext ("Fetching C \(cobj)") }
}
return msg
}
but this attempt failed, i.e., the following print() does not print anything.
getAll().subscribeNext {
print ($0)
}
How should I rewrite my logic?
Problem
It's because you're not retaining your Disposables, so they're being deallocated immediately, and thus do nothing.
In getAll, you create an Observable<AType> via get_A_objects(), yet it is not added to a DisposeBag. When it goes out of scope (at the end of the func), it will be deallocated. So { aobj in msg.onNext ("Fetching A \(aobj)") } will never happen (or at least isn't likely to, if it's async).
Also, you aren't retaining the ReplaySubject<String> returned from getAll().subscribeNext either. So for the same reason, this would also be a deal-breaker.
Solution
Since you want two Observables: one for the actual final results (Observable<CType>), and one for the progress status (ReplaySubject<String>), you should return both from your getAll() function, so that both can be "owned", and their lifetime managed.
func getAll() -> (Observable<CType>, ReplaySubject<String>) {
let progress = ReplaySubject<String>.createUnbounded()
let results = self.get_A_objects()......
return (results, progress)
}
let (results, progress) = getAll()
progress
.subscribeNext {
print ($0)
}
.addDisposableTo(disposeBag)
results
.subscribeNext {
print ($0)
}
.addDisposableTo(disposeBag)
Some notes:
You shouldn't need to use createUnbounded, which could be dangerous if you aren't careful.
You probably don't really want to use ReplaySubject at all, since it would be a lie to say that you're "fetching" something later if someone subscribes after, and gets an old progress status message. Consider using PublishSubject.
If you follow the above recommendation, then you just need to make sure that you subscribe to progress before results to be sure that you don't miss any progress status messages, since the output won't be buffered anymore.
Also, just my opinion, but I would re-word "Fetching X Y" to something else, since you aren't "fetching", but you have already "fetched" it.