How can I avoid reloading the whole table, disrupting the scroll - ios

I wanted to cap the number of elements to three. When I add element four, element one should disappear.
If I want scrolling to continue fine, I should tell UITableView that I am deleting rows in the entire section zero by calling deleteRowsAtIndexPaths. After that I need to tell UITableView that you are inserting a bunch of rows at the third section (section index 2).
This way you would be able to avoid reloading the whole table, disrupting the scroll.
But unfortunately it is not working for me.
Give me this error :
'attempt to delete row 3 from section 2, but there are only 1 sections before the update'
code :
//scrolling down time calling this method
func oldestState(){
//var students : [[Student]]? = [[],[],[]]
let data = getdata()
self.students?[(self.firstIndex+self.count) % (self.students?.count)!] = data
if (self.count != 3) {
self.count += 1
} else {
self.firstIndex = (self.firstIndex+1) % (self.students?.count)!
}
self.newsFeedTableView.beginUpdates()
let indexPathsDeleted = (0..<(data
.count)).map { IndexPath(row: $0, section: (self.students?.count)! - 1) }
self.newsFeedTableView.deleteSections([0], with: UITableViewRowAnimation.automatic)
self.newsFeedTableView.deleteRows(at: indexPathsDeleted, with: UITableViewRowAnimation.automatic)
self.newsFeedTableView.endUpdates()
self.newsFeedTableView.beginUpdates()
let indexPathsInsert = (0..<(data
.count)).map { IndexPath(row: $0, section: (self.students?.count)! - 1) }
self.newsFeedTableView.insertSections([2], with: UITableViewRowAnimation.automatic)
self.newsFeedTableView.insertRows(at: indexPathsInsert, with: UITableViewRowAnimation.automatic)
self.newsFeedTableView.endUpdates()
}
func getdata() -> [Student]{
var _students = [Student]()
for i in itemNumber..<(itemNumber + 4) {
let student = Student()
student.name = "\(i)"
print("adding student roll number : \(student.name)")
_students.append(student)
}
itemNumber += 4
return _students
}
}
Here is full Git code :

I don't know the dataSource of the tableview behave,but from the error,it tells us clearly,that you have only one section,so you can't handle the section 2,because there isn't section 2.You can only handle the section 0.
You can delete the whole rows,and add row in section 0.besides,You should handle the self.students array,let it match the tableview rows,or it will crash.
UPDATE:
This commit resolve the issue Table scrolling
I have correct it,This version.
This commit resolve the issue Table scrolling get jerky
I have correct it,This version.

Related

How to append row in tableview swift?

I'm adding data in my model and model is assigned to tableview to reload data. But every time reloading is not looking good. so I want just last element that was added in model, should be appended in already exist tableview. Tried so many ways but getting crash when my tableview is empty.
let lastSectionIndex = self.isGroupChat ? self.objGroupChatList!.count-1 : self.objSingleChatList!.count-1
var lastRow = 0
if self.isGroupChat {
lastRow = (self.objGroupChatList?[lastSectionIndex].count ?? 1)
} else {
lastRow = (self.objSingleChatList?[lastSectionIndex].count ?? 1)
}
let IndexPathOfLastRow = IndexPath(row: lastRow-1, section: lastSectionIndex)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPathOfLastRow], with: UITableViewRowAnimation.none)
self.tableView.endUpdates()
This is crashing with error:
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of sections. The number of sections contained in the table
view after the update (1) must be equal to the number of sections
contained in the table view before the update (0), plus or minus the
number of sections inserted or deleted (0 inserted, 0 deleted).'
You should use insertSections for new sections. insertRows only works for existing sections.
You need to do something like,
let section = 0 //get your section here...
dataSource[section].append("five")
let row = dataSource[section].count - 1
tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .none)
This is just an example of how you can get that working. Fill the gaps as per your code.

Nightmare with performBatchUpdates crash

I am facing a nightmare of a crash during performBatchUpdates on a collection view.
The problem is basically this: I have a lot of images on a directory on a server. I want to show the thumbnails of those files on a collection view. But the thumbnail have to be downloaded from the server asynchronously. As they arrive they will be inserted on the collection view using something like this:
dispatch_async(dispatch_get_main_queue(),
^{
[self.collectionView performBatchUpdates:^{
if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}
if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}
if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}
} completion:nil];
});
the problem is this (I think). Suppose that at time = 0, the collection view has 10 items. I then add 100 more files to the server. The application sees the new files and start downloading the thumbnails. As the thumbnails download they will be inserted on the collection view. But because the downloads can take different times and this download operation is asynchronous, at one point iOS will lost track of how many elements the collection has and the whole thing will crash with this catastrophic infamous message.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (213) must be equal to the number of
items contained in that section before the update (154), plus or minus
the number of items inserted or deleted from that section (40
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'
The proof I have something fishy is going on is that if I print the count of items on the data set I see exactly 213. So, the dataset matches the correct number and the message is nonsense.
I have had this problem before, here but that was an iOS 7 project. Somehow the problem returned now on iOS 8 and the solutions there are not working and now the dataset IS IN SYNC.
It sounds like you need to do a bit extra work with batching which images have appeared for each animation group. From dealing with crashes like this before, the way performBatchUpdates works is
Before invoking your block, it double checks all the item counts and saves them by calling numberOfItemsInSection (this is the 154 in your error message).
It runs the block, tracking the inserts/deletes, and calculates what the final number of items should be based on the insertions and deletions.
After the block is run, it double checks the counts it calculated to the actual counts when it asks your dataSource numberOfItemsInSection (this is the 213 number). If it doesn't match, it will crash.
Based on your variables insertedIndexes and changedIndexes, you're pre-calculating which things need to show up based on the download response from server, and then running the batch. However I'm guessing your numberOfItemsInSection method is always just returning the 'true' count of items.
So if a download completes during step 2, when it performs the sanity check in '3', your numbers won't line up anymore.
Easiest solution: Wait until all files have downloaded, then do a single batchUpdates. Probably not the best user experience but it avoids this issue.
Harder solution: Perform batches as needed, and track which items have already shown up / are currently animating separately from the total number of items. Something like:
BOOL _performingAnimation;
NSInteger _finalItemCount;
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _finalItemCount;
}
- (void)somethingDidFinishDownloading {
if (_performingAnimation) {
return;
}
// Calculate changes.
dispatch_async(dispatch_get_main_queue(),
^{
_performingAnimation = YES;
[self.collectionView performBatchUpdates:^{
if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}
if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}
if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}
_finalItemCount += (insertedIndexes.count - removedIndexes.count);
} completion:^{
_performingAnimation = NO;
}];
});
}
The only thing to solve after that would be to make sure you run one final check for leftover items if the last item to download finished during an animation (maybe have a method performFinalAnimationIfNeeded that you run in the completion block)
I think the problem is caused by the indexes.
Key:
For updated and deleted items, the indexes have to be the indexes of original items.
For inserted items, the indexes have to be the indexes of final items.
Here is a demo code with comments:
class CollectionViewController: UICollectionViewController {
var items: [String]!
let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))
items = before
}
func onRefresh(_: AnyObject) {
items = after
collectionView?.performBatchUpdates({
self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
// self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])
// NOTE: Have to be the indexes of original list
self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
// self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])
// NOTE: Have to be index of final list
self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])
}, completion: nil)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)
let label = cell.viewWithTag(100) as! UILabel
label.text = items[indexPath.row]
return cell
}
}
For anyone having a similar issue, let me quote the documentation on UICollectionView:
If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).
I was originally referencing an array of a separate model object, but decided to keep a local copy of the array within the view controller and update the array within performBatchUpdates(_:completion:).
Problem was solved.
This may be happening because you do need to also make sure with collectionViews to delete and insert sections. when you try to insert an item in a section that doesn't exist you will get this crash.
Preform Batch updates doesn't know that you meant to add section X+1 when you insert an item at X+1, X. without you already having added that section in.

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

Table is jumpy when scrolling to bottom of table

This question relates to another one of my questions, I'm using a similar explanation here:
I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.
The problem is that when I reach the bottom of the table and make a new network call for more data, the table jumps/jerks. It only happens here, not when I'm scrolling up and down.
I am using tableView.estimatedRowHeight = 100 and tableView.rowHeight = UITableViewAutomaticDimension which may be the problem based on numerous answers on Stack Overflow but I'm not sure how else to do this.
Any thoughts?
Update to question 1
I am not reloading the entire table. Instead, I used insertRowsAtIndexPaths as shown here:
func refreshItems(index: Int) {
// Make to network call to Google Books
GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in
guard let books = books else {
return
}
self.footerView.hidden = false
self.booksArrayFromNetworkCall += books
dispatch_async(dispatch_get_main_queue()) {
self.tableView.beginUpdates()
var indexPathsToInsert = [NSIndexPath]()
for i in self.currentIndexCount..<self.currentIndexCount + 10 {
indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .None)
self.tableView.endUpdates()
self.isLoading = false
self.currentIndexCount = self.booksArrayFromNetworkCall.count
}
}
}
If you are reloading the whole table, it might be the cause of the jumpiness. You could try reloading a section of it, in an animated way. I'm not sure, but that might help.
This is the code I use for reloading a section with an animation:
self.tableView.reloadSections(NSIndexSet(index: 0), withRowAnimation: .Automatic)

Iterate over all the UITableCells given a section id

Using Swift, how can I iterate over all the UITableCells given a section id (eg: all cells in section 2)?
I only see this method: tableView.cellForRowAtIndexPath, which returns 1 cell given the absolute index, so it doesn't help.
Is there an elegant and easy way?
Note: I want to set the AccesoryType to None for all of the cells in a section, programatically, say: after a button is clicked, or after something happends (what happends is not relevant for the question)
I have the reference for the UITableView and the index of the section.
You misunderstand how table views work. When you want to change the configuration of cells, you do not modify the cells directly. Instead, you change the data (model) for those cells, and then tell your table view to reload the changed cells.
This is fundamental, and if you are trying to do it another way, it won't work correctly.
You said "I need the array of cells before modifying them…" Same thing applies. You should not store state data in cells. As soon as a user makes a change to a cell you should collect the changes and save it to the model. Cells can scroll off-screen and their settings can be discarded at any time.
#LordZsolt was asking you to show your code because from the questions you're asking it's pretty clear you are going about things the wrong way.
EDIT:
If you are convinced that you need to iterate through the cells in a section then you can ask the table view for the number of rows in the target section, then you can loop from 0 to rows-1, asking the table view for each cell in turn using the UITableView cellForRowAtIndexPath method (which is different than the similarly-named data source method.) That method will give you cells that are currently visible on the screen. You can then make changes to those cells.
Note that this will only give you the cells that are currently on-screen. If there are other cells in your target section that are currently not visible those cells don't currently exist, and if the user scrolls, some of those cells might be created. For this reason you will need to save some sort of state information to your model so that when you set up cells from the target section in your datasource tableView:cellForRowAtIndexPath: method you can set them up correctly.
For Swift 4 I have been using something along the lines of the following and it seems to work pretty well.
for section in 0...self.tableView.numberOfSections - 1 {
for row in 0...self.tableView.numberOfRows(inSection: section) - 1 {
let cell = self.tableView.cellForRow(at: NSIndexPath(row: row, section: section) as IndexPath)
print("Section: \(section) Row: \(row)")
}
}
Im using same way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible :D
To answer my own question: "how can I iterate over all the UITableCells given a section id?":
To iterate over all the UITableCells of a section section one must use two methods:
tableView.numberOfRowsInSection(section)
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))
So the iteration goes like this:
// Iterate over all the rows of a section
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
var cell:Cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?
// do something with the cell here.
}
At the end of my question, I also wrote a note: "Note: I want to set the AccesoryType to None for all of the cells in a section, programatically". Notice that this is a note, not the question.
I ended up doing that like this:
// Uncheck everything in section 'section'
for (var row = 0; row < tableView.numberOfRowsInSection(section); row++) {
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: row, inSection: section))?.accessoryType = UITableViewCellAccessoryType.None
}
If there is a more elegant solution, go ahead and post it.
Note: My table uses static data.
Swift 4
More "swifty", than previous answers. I'm sure this can be done strictly with functional programming. If i had 5 more minutes id do it with .reduce instead. ✌️
func cells(tableView:UITableView) -> [UITableViewCell]{
var cells:[UITableViewCell] = []
(0..<tableView.numberOfSections).indices.forEach { sectionIndex in
(0..<tableView.numberOfRows(inSection: sectionIndex)).indices.forEach { rowIndex in
if let cell:UITableViewCell = tableView.cellForRow(at: IndexPath(row: rowIndex, section: sectionIndex)) {
cells.append(cell)
}
}
}
return cells
}
Im using this way of iterating all table view cells , but this code worked for only visible cells , so I'v just add one line allows iterating all table view cells wether visible they are or not
//get section of interest i.e: first section (0)
for (var row = 0; row < tableView.numberOfRowsInSection(0); row++)
{
var indexPath = NSIndexPath(forRow: row, inSection: 0)
println("row")
println(row)
//following line of code is for invisible cells
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Top, animated: false)
//get cell for current row as my custom cell i.e :roomCell
var cell :roomCell = tableView.cellForRowAtIndexPath(indexPath) as roomCell
}
* the idea is to scroll tableview to every row I'm receiving in the loop so, in every turn my current row is visible ->all table view rows are now visible
You can use
reloadSections(_:withRowAnimation:) method of UITableView.
This will reload all the cells in the specified sections by calling cellForRowAtIndexPath(_:). Inside that method, you can do whatever you want to those cells.
In your case, you can apply your logic for setting the appropriate accessory type:
if (self.shouldHideAccessoryViewForCellInSection(indexPath.section)) {
cell.accessoryType = UITableViewCellAccessoryTypeNone
}
I've wrote a simple extension based on Steve's answer. Returns the first cell of given type (if any) in a specified section.
extension UITableView {
func getFirstCell<T: UITableViewCell>(ofType type: T.Type, inSection section: Int = 0) -> T? {
for row in 0 ..< numberOfRows(inSection: section) {
if let cell = cellForRow(at: IndexPath(row: row, section: section)) as? T {
return cell
}
}
return nil
}
}

Resources