How to reload UITableView by use RxDataSources correctly? - ios

I'm trying to build a tableView witch has many cells with a button, what I want to do is when I click the button in a cell, the cell should be go to the bottom of the table, here's my code:
let datasource = RxTableViewSectionedAnimatedDataSource<ToDoListSection>(
configureCell: { [weak self] _, tableView, indexPath, item in
guard let self = self else { return UITableViewCell() }
let cell = tableView.dequeueReusableCell(withIdentifier: ToDoTableViewCell.reuseID, for: indexPath) as? ToDoTableViewCell
cell?.todoTextView.text = item.text
cell?.checkBox.setSelect(item.isSelected)
cell?.checkBox.checkBoxSelectCallBack = { selected in
if selected {
var removed = self.datasList[indexPath.section].items.remove(at: indexPath.row)
removed.isSelected = selected
self.datasList[indexPath.section].items.append(removed)
self.datasList[indexPath.section] = ToDoListSection(
original: self.datasList[indexPath.section],
items: self.datasList[indexPath.section].items
)
self.sections.onNext(datasList)
} else {
// Todo
}
}
return cell ?? UITableViewCell()
}, titleForHeaderInSection: { dataSource, section in
return dataSource[section].header
})
sections.bind(to: table.rx.items(dataSource: datasource))
.disposed(by: disposeBag)
however, because I sent a onNext event in the configureCell closure, I received a waring:
⚠️ Reentrancy anomaly was detected.
Debugging: To debug this issue you can set a breakpoint in /Users/me/Desktop/MyProject/Pods/RxSwift/RxSwift/Rx.swift:96 and observe the call stack.
Problem: This behavior is breaking the observable sequence grammar. next (error | completed)?
This behavior breaks the grammar because there is overlapping between sequence events.
Observable sequence is trying to send an event before sending of previous event has finished.
Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,
or that the system is not behaving in the expected way.
Remedy: If this is the expected behavior this message can be suppressed by adding .observe(on:MainScheduler.asyncInstance)
or by enqueuing sequence events in some other way.
and the action on the screen is not I want.
what should I do?
how to reload the TableView correctly?

The fundamental problem here is that you are calling onNext inside an Observer that is observing the emission. Another way to word this is that you are emitting a new value before the system is finished processing the current value.
As the warning says, the simplest way to deal with this (and likely the best way in this case) is to insert .observe(on:MainScheduler.asyncInstance) between sections. and bind(to:). What this does is stall the emission for a cycle so your configureCell function has a chance to return.

Related

What is "stable identifier" for SwiftUI Cell?

While listening Use SwiftUI with UIKit (16:54), I heard, that the reporter said:
"When defining swipe actions, make sure your buttons perform their actions using a stable identifier for the item represented.
Do not use the index path, as it may change while the cell is visible, causing the swipe actions to act on the wrong item."
- what??? All these years I was fighting with prepareForReuse() and indexPath in cells trying to somehow fix bugs related to cell reusing.
What is this "stable identifier"?
Why does no one talk about it?
On stackoverflow you can find answers only related to prepareForReuse() function. No "stable identifier".
Is it reuseIdentifier?
If so, how I suppose to use it?
Creating for each cell its own reuseIdentifier, like this:
for index in 0..<dataSourceArray.count {
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "ReuseIdForMyCell" + "\(index)")
}
She made a mistake, if you download the sample associated with the video you'll see the deleteHandler captures the item from outside already, it doesn't look it up again when the handler is invoked. She was trying to say that if you look up an item in the handler then there is a chance if other rows have been added or removed then its index will have changed so if you use the rows old index to look up the item then you would delete the wrong one. But since no lookup is required that will never happen, so she shouldn't have even mentioned it. Here is the code in question:
// Configures a list cell to display a medical condition.
private func configureMedicalConditionCell(_ cell: UICollectionViewListCell, for item: MedicalCondition) {
cell.accessories = [.delete()]
// Create a handler to execute when the cell's delete swipe action button is triggered.
let deleteHandler: () -> Void = { [weak self] in
// Make sure to use the item itself (or its stable identifier) in any escaping closures, such as
// this delete handler. Do not capture the index path of the cell, as a cell's index path will
// change when other items before it are inserted or deleted, leaving the closure with a stale
// index path that will cause the wrong item to be deleted!
self?.dataStore.delete(item)
}
// Configure the cell with a UIHostingConfiguration inside the cell's configurationUpdateHandler so
// that the SwiftUI content in the cell can change based on whether the cell is editing. This handler
// is executed before the cell first becomes visible, and anytime the cell's state changes.
cell.configurationUpdateHandler = { cell, state in
cell.contentConfiguration = UIHostingConfiguration {
MedicalConditionView(condition: item, isEditable: state.isEditing)
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: deleteHandler) {
Label("Delete", systemImage: "trash")
}
}
}
}
}

Problems with asynchronous data, UITableView, and reloadRowsAt

I'm trying to implement a tableView that has 4 different possible prototype cells. They all inherit from base UITableViewCell class and implement its protocol.
For two of the cells there's asynchronous data fetching but one in particular has been giving me fits. The flow is as follows:
1) Dequeue reusable cell
2) Call configure
func configure(someArguments: ) {
//some checks
process(withArguments: ) { [weak self in] in
if let weakSelf = self {
weakSelf.reloadDelegate.reload(forID: id)
}
}
}
3) If the async data is in the cache, configure the cell using the image/data/stuff available and be happy
4) If the async data is NOT in the cache, fetch it, cache it, and call the completion
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
//add to cache
//call completion
}
}
}
5) If the completion is called, reload the row in question by passing the index path up to the UITableViewController and calling reloadRows(at:with:)
func reload(forID: ) {
tableView.beginUpdates()
tableView.reloadRows(at: indexPath_matching_forID with: .automatic)
tableView.endUpdates()
}
Now, my understanding is that reloadRows(at:with:) will trigger another dataSource/delegate cycle and thus result in a fresh resuable cell being dequeued, and the configure method being called again, thereby making step #3 happy (the async data will now be in the cache since we just fetched it).
Except...that's not always happening. If there are cells in my initial fetch that require reloading, it works - they get the data and display it. Sometimes, though, scrolling down to another cell that requires fetching DOES NOT get the right data...or more specifically, it doesn't trigger a reload that populates the cell with the right data. I CAN see the cache being updated with the fresh data, but it's not...showing up.
If, however, I scroll completely past the bad cell, and then scroll back up, the correct data is used. So, what the hell reloadRows?!
I've tried wrapping various things in DispatchQueue.main.async to no avail.
reloadData works, ish, but is expensive because of potentially many async requests firing on a full reload (plus it causes some excessive flickering as cells come back)
Any help would be appreciated!
Reused cells are not "fresh". Clear the cell while waiting for content.
func process(withArguments: completion:) {
if let async_data = cache.exists(forID: async_data.id) {
//set labels, add views, etc
} else {
fetch_async_data() {
// ** reset the content of the cell, clear labels etc **
//add to cache
//call completion
}
}
}

Pass the result to a Button within a UITableViewCell after executing an on tap closure?

I'm looking for any possible way of passing the result of an networking update to a button in a UITableViewCell as a closure.
I have some UITableViewCells that are products. In these cells, I have an 'Add to Cart' button. I set a buttonTap closure in my UITableViewCell cellforRowAtIndexPath method, setup a touch handler within the cell for the button, and when that handler is called, execute the buttonTap closure. I handle my cart updating on a cart object which lives on the main controller.
The result of the cart update action returns true if they can add more items to their cart. Then, I update the button accordingly. I like this approach because I don't have to deal with delegates and I can keep all of the cart logic itself far far away from the cell; the cell just knows how to make a button enabled/disabled/loading/etc.
/// Buttom tap callback callback.
public typealias Selection = () -> Bool
class MealTableViewCell: UITableViewCell {
var buttonTap: Selection?
// Runs when tapping the button
func didTapAdd() {
if let buttonBlock = buttonTap {
self.button.isLoading = true
// Simulate loading
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.button.enabled = buttonBlock()
self.button.isLoading = false
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = .... (fetch cell)
cell.buttonTap = {
// returns true if the user can add more items to their cart
return self.cart.update(product: product, quantity: 1)
}
}
My question is that currently, my cart is all local with no API calls. I'm currently switching the cart over to an API driven one, with network calls to add and remove items. That means I can no longer return a BOOL, or return at all, from my cart.update(product:,quantity:) method as it is now an async call.
So, I can do something like rewrite that method signature to be
self.cart.update(product: product, quantity: 1, success: { canAddMore in
// API call succeeded
}, failure: { error in
// fall failed
})
The question is that how can I pass canAddMore to the tableViewCell? If I redefine what Selection means to take in a block that takes a bool as a param, I can't pass that param in from the controller as it would only be passed in when the block is executed on the cell itself.
How can I do something like
cell.buttonTap = {
cell.buttonTap = {
self.cart.update(product: product, quantity: 1), success: { canAddMore in
// !!!! What can I call here to pass canAddMore to the cell.
}, failure: { error in
}
}
}
canAddMore can be any value really, a BOOL is just this example. My big goal is to avoid coupling any knowledge of the cell's loading and buttons to the controller itself. If I use delegates, I would have to have a two way delegate makes the cell a delegate of the controller, and I've always felt that's the sort of wrong direction to approach this. I'm not positive it's really possible to pass the result of a closure back to the cell, but I am hoping there is!
EDIT: The big question I'm really trying to answer is if it is at all possible to pass data back to the cell (or any object) that originally called closure through that closure. There's a million ways to do data modifications in a table view, but that's sort of the main thing I'm trying to address.
I'm also "looking" to avoid storing the canAddMore state (e.g. the quantity remaining for that product) in the main 'products' array that powers the tableview. The initial state is set there, returned from a /products endpoint, but after that, inventory being available or not is returned by the carts API action.
I don't think you want to do what you think you want to do :)
In a nutshell, instead of trying to "talk back" to the cell that called the closure, you probably want to track the "canAddMore" state of each product in your Products data array, and then update the table row(s) when the state changes. So...
User taps "Add to Cart"
Give visual feedback in that row to show that you are processing the tap (gray out the button, or show a spinner, whatever looks good)
Call back to the closure to start the Add-to-cart API call
When the API call returns, update your local Products data to indicate the "canAddMore" state
reload the row(s) in the table to update the Button (make it active, inactive, change the title, whatever)
You almost certainly need to be doing something similar anyway, so the Buttons in each row will be updated when the user scrolls and the cells are reused.
A general approach is to update the cell's content with tableView.reloadRows(at: [indexPath], with: true) in your callback. This will call func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell for the specified cell. Don't forget that this has to be done in the main queue.

Incomplete or delayed label text in cell

I have the text label of a cell set by URL task. But when I first load up the app the label text is not set to the task results until I either scroll the table view or I select the cell itself.
I am assuming my code needs to manually "update" the view of the cell? Here is a task I am using inside of the tableView cellForRowAtIndexPath function:
if indexPath.row == self.homeLabels.count - 1 {
let task1 = NSURLSession.sharedSession().dataTaskWithURL(url1!) { data, response, error in
if error != nil {
println(error)
return
}
let parser = NSXMLParser(data: data)
parser.delegate = self
if parser.parse() {
println(self.results)
if indexPath.row == 0 {
cell.detailTextLabel!.text = self.parseResults
}
}
}
I've tried adding a tableView.reloadData() but that seems to not be the correct function.
Actually, it is not a good way to do this.
First, NSURLSession is an asynchronous networking, that means they run the networking off of the main thread.
Second, you should have a data model, like array to hold these strings from self.parseResults, and handle these requests in viewDidLoad.
Finally, check all the requests done or not. If done, than reload the tableview, feeding all data to UI.
You should do all UI updates on the main queue.
dispatch_async(dispatch_get_main_queue()) {
// Update UI
}

UITableView - how do you reload with completely new data?

I've got a table view showing the output of a search. When I update it to show the output of a totally different search if the old set of results was longer then old cells remain below my new ones.
For examples, if my first results are:
[Sam,
Joe,
Sally,
Betty,
Bob]
then I have five cells, one per result, as expected. If my second set of results is short, say just
[Smith]
then I now have five cells (Smith, Joe, Sally, Betty and Bob), when only one (Smith) is expected.
Here's how I'm reloading:
results = getResults()
tableView.reloadData()
And here's how I'm getting the number of cells:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if results != nil {
println("Table has \(results!.count) rows.")
return results!.count
}
println("Table is empty.")
return 0
}
which is printing out "Table has 1 rows." as expected, but the four old rows are still there.
Now, I could delete them before reloading, or delete the whole section, but is there a better way of achieving this? I thought reloadData would reload everything.
Additional Info
Here's cellForRowAtIndexPath as requested:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchEventsCell", forIndexPath: indexPath) as SearchEventsCell
if results != nil && eventStore != nil && results!.count >= indexPath.row {
let event = results![indexPath.row] as EKEvent
cell.configureCellWithEvent(event)
}
else {
println("Couldn't dequeue the cell")
}
return cell
}
And just to prove we have the right number of rows I put a println in before reloadData():
println("We're about to reload the table view, we have \(numberOfSectionsInTableView(tableView)) sections and \(tableView(tableView, numberOfRowsInSection:0)) rows in section 0")
tableView.reloadData()
Which outputs
Table has 1 rows.
We're about to reload the table view, we have 1 sections and 1 rows in sections 0
Table has 1 rows.
as it should.
Something else I've noticed, which surely has to be related - the table doesn't update at all until I try scrolling. What am I missing? I know reloadData has been called as println is being called within numberOfRowsInSection.
Update
The textFieldShouldReturn method that triggers the update includes this code:
eventStore.requestAccessToEntityType(EKEntityTypeEvent,
{ accessGranted, error in
if accessGranted {
if let searchEventsController = self.searchEventsController {
searchEventsController.search(self.searchTextField.text)
}
}
else {
self.accessDenied()
}
}
)
which seems very likely to be the culprit. Is there a better way of checking for permission? I included it there so that if the user ever disallowed it it would ask again next time they try to use it, rather than just failing.
The problem was indeed the fact that reloadData was taking place in another thread due to the eventStore.requestAccessToEntityType call.
There are two solutions:
1) Perform the permissions check once, when the app loads, instead of every time you access the the EventStore, as suggested by Paulw11. This means for the majority of the application there's only one thread.
2) Use the following code to execute reloadData on the main thread:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
as suggested by almas.
Update: I've just checked and if you revoke the permission for the app to access the Calendar then it doesn't ask the user again anyway, it just denies access, so there's no reason to keep the eventStore.requestAccessToEntityType where it is.

Resources