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.)
Related
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.
I perform many repeated requests in order to populate a field. I would like to cache the result and use the cached value the next time around.
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let item = itemCache[id] {
return Just(item).eraseToAnyPublisher()
}
return downloadItem(id: id)
.map { item in
if let item = item {
itemCache[id] = item
}
return item
}
.eraseToAnyPublisher()
}
}
func downloadItem(_ id: String) -> AnyPublisher<Item?, Never> { ... }
And this is called like this:
Just(["a", "a", "a"]).map(getItem)
However, all the requests are calling downloadItem. downloadItem does return on the main queue. I also tried wrapping the entire getItem function into Deferred but that had the same result.
First, the issue was that the function is being evaluated and only a publisher is returned. So the cache check is evaluated each time before the network publisher is ever subscribed to. Using Deferred is the proper fix for that. However, that still didn't solve the problem.
The solution was instead to first cache a shared publisher while the network request is pending so all requests during the network call will use the same publisher, then when it's complete to cache a Just publisher for the all future calls:
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let publisher = self.publisherCache[id] {
return publisher
}
let publisher = downloadItem(id)
.handleEvents(receiveOutput: {
// Re-cache a Just publisher once the network request finishes
self.publisherCache[id] = Just($0).eraseToAnyPublisher()
})
.share() // Ensure the same publisher is returned from the cache
.eraseToAnyPublisher()
// Cache the publisher to be used while downloading is in progress
self.publisherCache[id] = publisher
return publisher
}
One note, is that downloadItem(id) is async and being recieved on the main loop. When I replaced downloadItem(id) with Just(Item()) for testing, this didn't work beause the entire publisher chain was evaluated on creation. Use Just(Item()).recieve(on: Runloop.main) to fix that while testing.
I have a Tableview with somewhat complex operations where
sections are inserted and updated in realtime though GraphQl subscriptions.
At the moment I'm having some problems with race conditions.
When i receive new data through the subscription i parse it into my Model and update the tableview with insertions and updating the content and size on some existing cells.
The problem arrises when i get data faster then i can finish the previous update in the table resulting in an "invalid number of sections" crash.
I believe a solution is to serial/wait the sequence subscription -> model -> tableview.
Ive tried to get this to work with various concurrency methods such as, semaphore, sync, barrier, dispatch group. but have not been able to successfully figure it out.
If i try to simplify, the sequence of events transpires something like this.
//Model
subscription { data in
//should not start parsing new data until previous data has been drawn in table to avoid missmatch in model and table
parse(data)
}
func parse(data) {
//do stuff like update datamodel
figureOutWhatToUpdateInTable(data) { (insertSet, reloadSet) in
delegate.updateTableView(insertSet, reloadSet)
}
//do stuff
}
//VC
func updateTableView(insertSet, reloadSet) {
tableView.beginUpdates()
CATransaction.begin()
//once a new section is inserted we need to update content of some sections
CATransaction.setCompletionBlock {
reloadSet.forEach { (index: Int) in
let section = tableView.headerView(forSection: index)
section.updateData(data[index]) {
// call begin/end to make tableview get height
tableView.beginUpdates()
tableView.endUpdates()
// now im ready to parse new data into my model
}
})
}
tableView.insertSections(insetSet, with: .top)
CATransaction.commit()
tableView.endUpdates()
}
basically i need to wait for section.updateData to finish before parse(data) processes any new data from the subscription
You can use at this case DispatchGroup
let group = DispatchGroup()
group.enter()
// do some stuff (1)
group.leave()
group.notify(queue: .main) {
// do stuff after 1
}
After call group.leave() group avtomaticaly execute STUFF at group.notify Block
I have some doubts in my approach. I have two type of Observables:
//I can fetch from the server the houses and save them in the database
func houses() -> Observable<[House]>
//Given a houseId, I can fetch it's details and save them in the database
func houseDetail(id: Int) -> Observable<Family>
I would like to do an Observable which first fetch all houses, and then fetch the families. What I did is something like that:
//I am an observable which, when I complete, all data are saved
func fetchAllHousesAndFamily() -> Observable<Any> {
var allHousesObservables: [Observable] = []
for house in houses {
allHousesObservables.append(houseDetail(house.id))
}
return Observable.combineLatest(allHousesObservables)
}
But this for...it doesn't seem to be reactive style to me, and it seems like a hack because I don't know enough about rx operators.
Do you have the right way to do it in the rxworld ?
Thank you
To get all family from the result of houses, you will want to use the flatMap operator. flatMap accepts a closure with signature T -> Observable<U>, so, in our specific example, it will be a function of type [House] -> Observable<Family>.
houses().flatMap { houses in
let details: [Observable<Family>] = houses.map { houseDetail($0.id) } // [1]
return Observable.from(details).merge() // [2]
}
[1]: We are mapping an array of house to an array of Observable<Family>.
[2]: from(_:) converts [Observable<Family>] to Observable<Observable<Family>>. merge() then transform it to Observable<Family>
We now have an observable that will emit one next event for each house with its family details.
If we'd prefer to keep the House value around, we could simply map again on the first line, like so:
let details: [Observable<(House, Family)>] = house.map { house in
houseDetail(house.id).map { (house, $0) }
}
I am beginner in RxSwift in general trying to chain two different operation with my REST API to get all detailed products.
I wrapped some REST API to return RxSwift Observable, one return a list of product and other the product detail.
class API {
func listProduct() -> Observable<[Product]> { ... }
func detailProdcut(code: Int) -> Observable<[ProductDetail]> { ... }
}
Now I want to get the product detail from a product list, how can I do this in Rx way ? I'm trying to do something like
API.init().listProduct()
.flapMap { products -> <Product> in return products[0] }
.map { product in API.init(product.code) }
.merge()
.toArray
But isn't work, and I very confused about how to transform one list of products code into an array of products detail
let api = API()
let productDetails = api.listProducts()
.flatMap { products in
let productsObservable = Observable.from(products)
let productDetails = productsObservable.flatMap { api.detail(product($0.code) }
return productDetails.toArray()
}
What is going on here :
flatMap has specialized type [Product] -> Observable<[ProductDetails]>. How? Read on.
Observable.from takes a swift array and transforms it into an observable. It will emit next event for each of the element of the array. So we now have Observable<Product> in productsObservable.
the call to flatMap on productObservable will create an Observable<ProductDetail> for each next event it sends. We now have an observable that will send as many next event as their are products in the result of listProducts.
We use toArray to transform this observable to something that will emit only one next event, an array aggregating all the results from productDetails.