Table is jumpy when scrolling to bottom of table - ios

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)

Related

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

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.

Swift UITableView - How to put new items on top not on the bottom of the list?

I am new in Swift. The sample codes about UITableView show that new item is placed on the bottom of the list. How may we reverse this? I searched internet but could not find an answer.
Thanks.
Guven
UITableviews show data based on the order of an array such as an array of characters like
var data = ["B","C","D","E"]
Typically, you add data into array by using append which adds data at the end of the array, hence why it adds it at the bottom of the list.
data.append("A")
If you want your table view to add data on top of the list, then you can add your data at the beginning of the array like this.
data.insert("A", at: 0)
now reload your tableView, and new data would be added at the top of the list
yourTableViewName.reloadData()
To put new item on top, insert it at desired position (index 0) and reload corresponding indexPath (row: 0, section: 0).
let indexPathOfFirstRow = NSIndexPath.init(forRow: 0, inSection: 0)
yourArray.insert("some element", atIndex: 0)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathOfFirstRow], withRowAnimation: .Automatic)
tableView.endUpdates()
Reloading whole table is a costly task and not recommended.
Add item to the array last index .
Then reload tableView
All you really need to do is to .insert your object at index 0 rather than append. Appending your latest value at the first index 0 ensures your last data is shown at the top. You can continue using tableview.reloaddata()as usual. Hope the screenshot helps
Inserting new items on data array at 0 index is easy but keeping the state of tableview as it is difficult especially when you are dealing with headers too...
I am using this to append old messages when paginating the chat...
you can iterate on data and insert new items and header with data.insert("new message", at: 0) but tableview automatically going to jump on zero index. to keep that state you should need a unique id in every new item in my case it was time and messageId
I am saving the last message id before appending new items ... and using that id to calculate the indexPath of the previous top message cell and scroll to it with no animation...
func getIndexPath(WithMessageId id: Double) -> IndexPath? {
for (section, sectionObjects) in messagesArray.enumerated() {
if let index = sectionObjects.Messages?.firstIndex(where: { $0.MessageID == id }) {
return IndexPath(row: index, section: section)
}
}
return nil
}
this method returns the indexpath use that as follows
self.tableView.reloadData()
self.tableView.scrollToRow(at: getIndexPath(WithMessageId: lastMessageID) ?? IndexPath(row: 0, section: 0),
at: .top, animated: false)
its gonna append new items without even any glitches like Twitter appending new news feeds on top...

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

How to make an Expandable TableView with Profile Img?

I was trying to create a Slide side out menu using the SWRevealViewController but I could not do the expandable TableView and the profile with image + Hello, Name...
And all I could do was:
Basically how do you make an expandable TableView and the profile.
Are there any examples of how to do this?
From what I can tell, you want an expandable table view, ie: Click a row in the table, and more options will appear.
I've achieved that in the past using the following:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? OuterCell{
if let object = data[indexPath.row] as? OuterObject{
if(object.open == false){
data.insert(InnerObject(content: object.innerContent), atIndex: indexPath.row + 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: indexPath.row + 1, inSection: 0)], withRowAnimation: .Left)
tableView.endUpdates()
}else{
data.removeAtIndex(indexPath.row + 1)
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: indexPath.row + 1, inSection: 0)], withRowAnimation: .Left)
tableView.endUpdates()
}
object.open = !object.open
}
}
}
Basically what is happening here is the dataSource for the TableView is represented by the data array in this example. This is what you connect to all the TableViewDelegate hooks.
Within the data array I am holding two types of objects, a OuterObject and an InnerObject.
The OuterObject contains all the information for the inner objects that it contains, and if you click on it, it will dynamically add its children into the data array.
Example Data (I'm using JSON cause its easy to read and understand, you'll have to represent this in swift)
[
{
name="Ensino",
type="outerObject",
open=true
childObjects=[
{
name="Servico",
type="innerObject"
},
{
name="Avaliacao",
type="innerObject"
}
]
},
{
name="Pesquisa",
type="outerObject",
open=false
childObjects=[]
}
]
So in your case you would need an "Ensino" object, with a children property containing data for: Avaliaco, Servicos, ect...
Outer Object also keeps state as to whether or not it is open or closed, that way if you click on it again, you can know to remove all the inner objects that belong to it.
In this example I have hardcoded the OuterObject to always have at most one child, you would have to modify the code to be smarter as to how many objects are inserted/deleted upon click to be based on how many children objects the outer object you click has.
By calling tableView.beginUpdates(), tableView.beginUpdates(), tableView.insertRowsAtIndexPaths() you are accessing the methods that will allow an animated transition to happen when you update the data.
The key to solving this problem is creating an intelligent data source, and then leveraging the methods that Apple has in UITableView to make it happen.
Let me know if you need more help, this is an advanced TableView topic.
check this sample code dropdown menu
this might give you some idea.

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