Swift Combine two handlers for one property - ios

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.

Related

How to force an initial value when creating a pipe with CurrentValueSubject in Combine in Swift 5?

I am trying to fetch initial value of EventDetailsModel and subscribe to all future updates.
When I call eventDetails(..), all the publishers already have some current value in them (i.e. chatModels and userModels have 10+ items); the problem is that because there are no new updates, the resulting pipe never returns EventDetailModels since .map(...) never gets called.
How can I make the combine pipe do at least one pass through the existing values when I am constructing it so my sink has some initial value?
var chatModels: Publishers.Share<CurrentValueSubject<[ChatModel], Never>> = CurrentValueSubject([]).share()
var userModels: CurrentValueSubject<[String: UserModel], Never> = CurrentValueSubject([:])
func eventDetails(forChatId chatId: String) -> AnyPublisher<EventDetailsModel?, Never> {
return chatModels
.combineLatest(userModels)
.map({ (chatList, userModels) -> EventDetailsModel? in
// Never gets called, even if chatModels and userModels has some existing data 😢
if let chatModel = (chatList.first { $0.id == chatId}) {
return EventDetailsModel(chatModel, userModels)
}
return nil
})
.eraseToAnyPublisher()
}
With combineLatest, you won't get any events until there is a latest for both publishers. That is what combineLatest means. The problem here is not chatModels, which does have a latest. The problem is userModels. Until it publishes for the first time, you won't get any events in this pipeline.
EDIT Okay, so now you've updated your code to reveal that both your publishers are CurrentValueSubjects. Well, in that case, you do get an initial event, as this toy example proves:
var storage = Set<AnyCancellable>()
let sub1 = CurrentValueSubject<Int,Never>(1)
let sub2 = CurrentValueSubject<String,Never>("howdy")
override func viewDidLoad() {
super.viewDidLoad()
sub1.combineLatest(sub2)
}
So if that isn't happening for you, the problem lies elsewhere. For example, maybe you forgot to store your pipeline, so you can't get any events at all. (But who knows? You have concealed the relevant code.)

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.

Concern about memory when choosing between notification vs callback closure for network calls?

Many posts seem to advise against notifications when trying to synchronize functions, but there are also other posts which caution against closure callbacks because of the potential to inadvertently retain objects and cause memory issues.
Assume inside a custom view controller is a function, foo, that uses the Bar class to get data from the server.
class CustomViewController : UIViewController {
function foo() {
// Do other stuff
// Use Bar to get data from server
Bar.getServerData()
}
}
Option 1: Define getServerData to accept a callback. Define the callback as a closure inside CustomViewController.
Option 2: Use NSNotifications instead of a callback. Inside of getServerData, post a NSNotification when the server returns data, and ensure CustomViewController is registered for the notification.
Option 1 seems desirable for all the reasons people caution against NSNotification (e.g., compiler checks, traceability), but doesn't using a callback create a potential issue where CustomViewController is unnecessarily retained and therefore potentially creating memory issues?
If so, is the right way to mitigate the risk by using a callback, but not using a closure? In other words, define a function inside CustomViewController with a signature matching the getServerData callback, and pass the pointer to this function to getServerData?
I'm always going with Option 1 you just need to remember of using [weak self] or whatever you need to 'weakify' in order to avoid memory problems.
Real world example:
filterRepository.getFiltersForType(filterType) { [weak self] (categories) in
guard let strongSelf = self, categories = categories else { return }
strongSelf.dataSource = categories
strongSelf.filteredDataSource = strongSelf.dataSource
strongSelf.tableView?.reloadData()
}
So in this example you can see that I pass reference to self to the completion closure, but as weak reference. Then I'm checking if the object still exists - if it wasn't released already, using guard statement and unwrapping weak value.
Definition of network call with completion closure:
class func getFiltersForType(type: FilterType, callback: ([FilterCategory]?) -> ()) {
connection.getFiltersCategories(type.id).response { (json, error) in
if let data = json {
callback(data.arrayValue.map { FilterCategory(attributes: $0) } )
} else {
callback(nil)
}
}
}
I'm standing for closures in that case. To avoid unnecessary retains you just need to ensure closure has proper capture list defined.

Capturing closure values in Swift

My question is very similar to several others here but I just can't get it to work. I'm making an API call via a helper class that I wrote.
First I tried a standard function with a return value and the result was as expected. The background task completed after I tired to assign the result.
Now I'm using a closure and I can get the value back into my view controller but its still stuck in the closure, I have the same problem. I know I need to use GCD to get the assignment to happen in the main queue.
this is what I have in my view controller
var artists = [String]()
let api = APIController()
api.getArtistList("foo fighters") { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
print("in the closure: \(artists)")
}
}
}
print ("method 1 results: \(artists)")
as the results are:
method 1 results: []
in the closure: [Foo Fighters & Brian May, UK Foo Fighters, John Fogerty with Foo Fighters, Foo Fighters, Foo Fighters feat. Norah Jones, Foo Fighters feat. Brian May, Foo Fighters vs. Beastie Boys]
I know why this is happening, I just don't know how to fix it :( The API calls need to be async, so what is the best practice for capturing these results? Based on what the user selects in the table view I'll be making subsequent api calls so its not like I can handle everything inside the closure
I completely agree with the #Craig proposal of the use of the GCD, but as your question involves the request of the API call every time you select a row, you can do the following:
Let's suppose you use the tableView:didSelectRowAtIndexPath: method to handle the selection, then you can do the following inside it:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// it is just a form to get the item
let selectedItem = items.objectAtIndex(indexPath.row) as String
api.getArtistList(selectedItem) { (thelist) -> Void in
if let names = thelist {
dispatch_async(dispatch_get_main_queue()) {
artists = names
}
}
}
}
And then you can observe the property and handle do you want inside it :
var artists: [String] = [] {
didSet {
self.tableView.reloadData() // or anything you need to handle.
}
}
It just another way to see it. I hope this help you.
The easy solution is to do whatever you're doing at your print(), inside the closure.
Since you're already dispatch_asyncing to the main queue (the main/GUI thread), you can complete any processing there. Push a new view controller, present some modal data, update your current view controller, etc.
Just make sure that you don't have multiple threads modifying/accessing your local/cached data that is being displayed. Especially if it's being used by UITableViewDelegate / UITableViewDataSource implementations, which will throw fits if you start getting wishy-washy or inconsistent with your return values.
As long as you can retrieve the data in the background, and the only processing that needs to occur on the main thread is an instance variable reassignment, or some kind of array appending, just do that on the main thread, using the data you retrieved on the back end. It's not heavy. If it is heavy, then you're going to need more sophisticated synchronization methods to protect your data.
Normally the pattern looks like:
dispatch_async(getBackgroundQueue(), {
var theData = getTheDataFromNetwork();
dispatch_async(dispatch_get_main_queue() {
self.data = theData // Update the instance variable of your ViewController
self.tableView.reloadData() // Or some other 'reload' method
});
})
So where you'd normally refresh a table view or notify your ViewController that the operation has completed (or that local data has been updated), you should continue your main-thread processing.

Resources