I have a table view which received data from a real-time database. These data are added from the bottom of the table view and so on this table view has to scroll down itself to show new data.
I've found a method to do it however I'm not satisfied because the scroll always start from the top of the list. Not very beautiful.
Here is the code of this method :
func tableViewScrollToBottom(animated: Bool) {
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
let numberOfSections = self.clientTable.numberOfSections
let numberOfRows = self.clientTable.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.clientTable.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Bottom, animated: animated)
}
})
}`
Is there a way to modify this method in order to scroll only from the previous position ?
The issue is probably how the rows were inserted into the table. For example, if you add rows to the end using something like this, you get a very smooth UI:
#IBAction func didTapAddButton(sender: AnyObject) {
let count = objects.count
var indexPaths = [NSIndexPath]()
// add two rows to my model that `UITableViewDataSource` methods reference;
// also build array of new `NSIndexPath` references
for row in count ..< count + 2 {
objects.append("New row \(row)")
indexPaths.append(NSIndexPath(forRow: row, inSection: 0))
}
// now insert and scroll
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .None)
tableView.scrollToRowAtIndexPath(indexPaths.last!, atScrollPosition: .Bottom, animated: true)
}
Note, I don't reload the table, but rather call insertRowsAtIndexPaths. And I turned off the animation because I know they're off screen, and I'll then scroll to that row.
Related
I'm building an app where rows containing messages are inserted at the end of a table
messages.append(message)
let indexPath:IndexPath = IndexPath(row:(messages.count - 1), section:0)
tableView.insertRows(at: [indexPath], with: .none)
I then scroll to the bottom of the table
DispatchQueue.main.async {
let indexPath = IndexPath(
row: self.numberOfRows(inSection: self.numberOfSections - 1) - 1,
section: self.numberOfSections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
Like other messaging apps, I would like to modify this so that the autoscrolling only happens if you're already scrolled at the end of the table instead of every time a new message gets inserted.
I've tried several techniques like detecting if the last cell is full visible https://stackoverflow.com/a/9843146/784637, or detecting when scrolled to the bottom https://stackoverflow.com/a/39015301/784637.
However my issue is that because scrollToRow sets animated:true, if a new message comes in but the previous message which came a split second before is still being scrolled down to via scrollToRow, then the autoscrolling to newest message and subsequent messages doesn't occur - ex. the last cell won't be fully visible until the animation is complete, or detecting if you're scrolled to to the bottom will be false until the animation is complete.
Is there any way I can get around this without setting animated: false?
What I would do is insert the row in a batch operation to make use of its completion handler, which serializes the UI update. Then I would check to see which rows are visible to the user and if the last couple of rows are, I think the user is close enough to the bottom of the conversation to force a down scroll.
let lastRow = IndexPath(row: messages.count - 1, section: 0)
DispatchQueue.main.async {
self.tableView.performBatchUpdates({
self.tableView.insertRows(at: [lastRow], with: .bottom)
}, completion: { (finished) in
guard finished,
let visiblePaths = self.tableView.indexPathsForVisibleRows else {
return
}
if visiblePaths.contains([0, messages.count - 2]) || visiblePaths.contains([0, messages.count - 1]) { // last 2 rows are visible
DispatchQueue.main.async {
self.tableView.scrollToRow(at: lastRow, at: .bottom, animated: true)
}
}
})
}
I'm currently using automatic scroll to bottom in UITableView, but there is a bug to solve:
If I press in any location of my UITableView before the scroll is executed that stop to work. (I cannot scroll to bottom before the view is displayed because I retrieve async data)
So the first thing I thought is tho disable the userInteraction like this:
override func viewDidLoad() {
super.viewDidLoad()
self.view.userInteractionEnabled = false
...
}
And to unlock it here:
func tableViewScrollToBottom(animated: Bool) {
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
let numberOfSections = self.tableView.numberOfSections
let numberOfRows = self.tableView.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: animated)
}
self.view.userInteractionEnabled = true
})
}
But (it's really strange) is not working, yes the view is disabled (I've tried even to lock the UITableView) and I can not scroll the UITableView but it seems that it can retrieve anyway the touch.
I have a PFTableViewController with PFTableViewCells in Swift.
In each TableViewCell (with a height of 80% the screen size), there is a button. I would like that when a user select the button, there is an automatic scroll to the next TableViewCell.
Any idea how I can make it?
Thanks a lot
Just determine the indexPath and scroll to the next indexPath. This example assumes a flat table without sections.
func buttonTapped(sender: UIButton) {
let point = sender.convertPoint(CGPointZero, toView:tableView)
let indexPath = tableView.indexPathForRowAtPoint(point)!
// check for last item in table
if indexPath.row < tableView.numberOfRowsInSection(indexPath.section) {
let newIndexPath = NSIndexPath(forRow:indexPath.row + 1 inSection:indexPath.section)
tableView.scrollToRowAtIndexPath(
newIndexPath, atScrollPosition:.Top, animated: true)
}
}
Is there a way to keep count of how many times a user taps a button? i am hoping to perform a segue after the user taps a button so many times. As of right now I am having my backend keep track of how many taps.
#IBAction func nextbuttontest(sender: AnyObject) {
let button = sender as! UIButton
let view = button.superview!
let cell = view.superview as! NewFeedControllerCell
let indexPath = tableView.indexPathForCell(cell)
let row = indexPath?.row
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: row!+1, inSection: 0),
atScrollPosition: UITableViewScrollPosition.Top,
animated: false)
}
You can create integer variable inside you view controller, and use it as a counter -> that is, when user taps increment it and if you have enough taps perform a segue or anything else.
For instance:
var tapCounter = 0
as a declaration in your view controller.
func tapGestureForElement(gest:UIGestureRecognizer){
tapCounter = tapCounter + 1
if(tapCounter == 123){
//performSegue
}
}
EDIT::
for the second part:
if(row!+1 == yourArray.count){ // or == 10, as you mentioned in comments, but I think that is a bad practice
//do nothing or do something, up to you :)
{
else{
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: row!+1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)
}
Edit 2:
you can even do something better, you can check if you reached end of the rows in section:
if( row!+1 == tableView. numberOfRowsInSection(indexPath.section)
Last part is not tested, but that should be it.
Like NickCatib said, add a tapCounter property to your view controller. However, instead of manually firing the segue when the tap count reaches the desired value, override shouldPerformSegueWithIdentifier:sender: so that it won't return YES for that particular segue until the tap count has been reached.
I'd like to visually scroll through my whole tableView. I tried the following, but it doesn't seem to perform the scrolling. Instead it just runs through the loops. I inserted a dispatch_async(dispatch_get_main_queue()) statement, thinking that that would ensure the view is refreshed before proceeding, but no luck.
What am I doing wrong?
func scrollThroughTable() {
for sectionNum in 0..<tableView.numberOfSections() {
for rowNum in 0..<tableView.numberOfRowsInSection(sectionNum) {
let indexPath = NSIndexPath(forRow: rowNum, inSection: sectionNum)
var cellTemp = self.tableView.cellForRowAtIndexPath(indexPath)
if cellTemp == nil {
dispatch_async(dispatch_get_main_queue()) {
self.tableView.scrollToRowAtIndexPath(indexPath!, atScrollPosition: .Top, animated: true)
self.tableView.reloadData()
}
}
}
}
}
I found a solution. Simply use scrollToRowAtIndexPath() with animation. To do so I had to create a getIndexPath() function to figure out where I want to scroll. Has more or less the same effect as scrolling through the whole table if I pass it the last element of my tableView.
If you want it to happen slower with more scrolling effect, wrap it inside UIView.animateWithDuration() and play with 'duration'. You can even do more animation if you want in its completion block. (No need to set an unreliable sleep timer, etc.)
func animateReminderInserted(toDoItem: ReminderWrapper) {
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: true)
}
}