Error updating UITableView after smaller sized JSON response? - ios

Currently, I have my app hitting my endpoints, and getting back 10 or less items. In the event the items returned is less than 10, and the UITableView is already showing 10 items, reloadData() will cause an error because the size is not the same as it was last time. Right now, when I get my response all I do is:
tableView.beginUpdates()
self.items = items //where self.items is the array that backs the UITableView, and items are the items I got back in form of JSON from the server.
tableView.reloadData()
tableView.endUpdates()

Why will it cause error? If you are using tableView.reloadData(), you don't need to call endUpdates() and beginUpdates(). Just simply assign the items and reload tableView data.
self.items = items
tableView.reloadData()
If you are on the background thread call the above code on the main queue.
dispatch_async(dispatch_get_main_queue()) { () -> Void in
// Code runs on main queue!
}

It is not your calling reloadData that caused error. It is because your numberOfRowsInSection or numberOfSectionsInTableView didn't return right value after you update your model.

In the table view method,
(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {}
you should write as , return [self.items count];
This way, your table will know, how many items your table need to show, and reload data would not complain.

Related

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
}
}
}

Reloading data from a tableView within a collectionViewCell

I have a tableView inside a collectionViewCell and I get an error when I try to reload the data.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.6.21/UITableView.m:1610
I tried using Dispatch.main.async and it seems to get rid of the problem. The only thing is that it doesn't reload the data and nothing changes in the tableView
func cleanItems(completion: #escaping (Bool) -> Void) {
Dispatch.main.async {
cell.tableView.beginUpdates()
// Make changes in the Data Source
cell.tableView.deleteRows(at: selectedItems, with: .fade)
cell.tableView.endUpdates()
// Reloading sections accordingly depending on what the user has deleted
// Do I need to reload data here again? It used to work without Dispatch, but it wasn't stable
cell.tableView.reloadData()
// Updating items with if statements to reload data in Firebase
completion(true)
}
}
This doesn't reload the data at all and nothing seems to change. The good thing is that I don't get a random crash, which was the case before implementing Dispatch.main.async
I've retrieved the numberOfRows in each section to see how many rows there are after ending updates.
print(cell.tableView.numberOfRows(inSection: 1))
and I get the same number of rows that are in the current view.
This is crucial, because if the tableView sections are all zero, the collectionViewCell should disappear. And we never get here in the completion block as it says that the numberOfRows has never changed. Leaving us with a non updated tableView.
I solved this by moving Dispatch.main.async outside the function call.
Dispatch.main.async {
cleanItems(completion: { (success) in
etc.
})
}

Swift, adding a class array into a variable does not update array content in original class

I'm new to IOS so forgive me for my coding mistakes. I'm facing an issue where I have a tableView Controller with two sections. The first section has a button, when clicked, appends data into an array and deletes it's own row in the first section (i did this as there are extra non related rows in the first section). The number of rows in the second section is based upon array.count.
My issue is that I tried begin/end update, and it still doesn't work. Whenever I run the code below and run the startNewDay function (when the button is clicked), this error occurs:
'attempt to insert row 0 into section 1, but there are only 0 rows in section 1 after the update'
This doesn't make any sense, as I appended the array already before I inserted the new rows. The array was empty before I appended it. Shouldn't there be the same number of rows in the second section as array.count?
Table View Delegate code:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if dataModel.lists[0].dayHasStarted == false {
return 2
} else {
return 1
}
} else {
if itemDoneCount == dataModel.lists[0].item.count && dataModel.lists[0].doneButtonVisible {
return dataModel.lists[0].item.count + 1
} else {
return dataModel.lists[0].item.count
}
}
}
startNewDay button function when pressed:
#IBAction func startNewDayDidPress(sender: AnyObject) {
dataModel.lists[0].dayHasStarted = true
dataModel.lists[0].startDate = NSDate()
addItemButton.enabled = !addItemButton.enabled
// deleting start new day button
let indexPath = NSIndexPath(forRow: 1, inSection: 0)
let indexPaths = [indexPath]
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
// Inserting new array elements and rows into 2nd section
let ritualsArray = dataModel.lists[0].rituals
var itemsArray = dataModel.lists[0].item
itemsArray.appendContentsOf(ritualsArray)
tableView.beginUpdates()
var insertRitualsArray = [NSIndexPath]()
for item in itemsArray {
let itemIndex = itemsArray.indexOf(item)
let indexPath = NSIndexPath(forRow: itemIndex!, inSection: 1)
insertRitualsArray.append(indexPath)
}
tableView.insertRowsAtIndexPaths(insertRitualsArray, withRowAnimation: .Top)
tableView.endUpdates()
}
SOLVED
The problem of this code is not at all related to the previous title of this thread, which may be misleading to people having the same issue as mine. Hence, I will be changing it. The previous title (for the curious) was :
"tableView.begin/end update not updating number of rows in section"
Just for others who might come across this issue, the issue isn't in the tableView delegate, nor is it in reloading the tableview data. For readability, I placed both dataModel.list[0].item into itemsArray and dataModel.list[0].item into ritualsArray. This apparently updates the itemsArray when appended but not the initial dataModel.list[0].item instead, which caused the second section in the tableView not to load the new number of rows, causing the error when inserting rows into non-existant rows.
Hence instead of:
let ritualsArray = dataModel.lists[0].rituals
var itemsArray = dataModel.lists[0].item
itemsArray.appendContentsOf(ritualsArray)
this solved it:
dataModel.list[0].item += dataModel.list[0].rituals
Hope it helps any beginner like me out there that comes across this issue.
Latest update
I found out recently that an array is of value type, and not reference type. Hence placing an array into a variable makes a copy of that array instead of serving as a placeholder for the original array.
Beginner mistake opps.
The error you are receiving means that the datasource contains a different number of items to however many there would be after inserting or deleting rows. This probably means that the data are not being inserted into your datasource array, or that the data do not match the criteria in the if statements in your numberOfRowsInSection function. To troubleshoot this, you should log the contents of the datasource array after modifying it to check what its contents are. If they are what you are expecting (I.e. The data have been added correctly) then the issue is in the way you are evaluating its contents to establish the number of rows. If the contents are not what you are expecting, then the issue is in the way you are inserting the data into the datasource array.
I had a similar problem after deleting a row. It seems that if
numberOfRowsInSection is not coherent (equal to last value -1) this error appears.
I see that there's a condition in your numberOfRowsInSection, this is perhaps the culprit

Inserting a new cell into a collectionview

I try to add a new cell in my UIcollectionView but it doesn't work.
In fact I send a post request to my API, I wait for the response, when I get the response, I put de JSON content into an NSDictionnary.
My CellForItemAtIndexPath Datasource method loads cell content from an array called "formationsData", so when I get the response, I add my NSDictionnary into this array.
In numberOfItemAtIndexPath I return something like : [self.formationData count].
Here is my code when I post my request :
NSDictionary * response = [responseJSON valueForKey:#"formation"];
[(NSMutableArray *)_formationsView.carousel.formationsData addObject:response];
[_formationsView.carousel insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem: _lastRowIndex-1 inSection:0]]]; //insert at index : last row - 1
//[_formationsView.carousel reloadData]
Before I was using reloadData but my app crash when I do that.
My problem :
Before I was using reloadData but my app crash when I do that.
Then that's the problem you need to solve. Just adding an item to the array you use to populate the collection view isn't enough to make a cell appear. The collection view doesn't have any idea where the data source gets its data; when there's a change to the data, you need to call -reloadData or similar.
If -reloadData causes a crash, then you need to figure out why and fix it.

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