Coldsteel48
Coldsteel48

Reputation: 3512

UITableView Refresh without scrolling

I have a _TableView with items , and I want to set automatic refresh,and I don't want it to scroll on refresh , lets say user scrolled 2 pages down , and the refresh trigered -> so I want to put the refreshed content to the top of the table without interupting user's scrolling

Assume user was on row 18 and now the _dataSource is refreshed so it fetched lets say 4 items , so I want user to stay on the item he was.

What would be the best approach to achieve it ??

Upvotes: 31

Views: 53595

Answers (13)

Ilias Karim
Ilias Karim

Reputation: 5371

Try the following.

tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0

Source: https://developer.apple.com/forums/thread/86703

Upvotes: 0

Asi Givati
Asi Givati

Reputation: 1475

i wrote something that works perfect for me:

extension UIScrollView {
    func reloadDataAndKeepContentOffsetInPlace(reloadData:(() -> Void)) {
    let currentContentHeight = contentSize.height
    if currentContentHeight == .zero {
       reloadData()
       return
    }
    reloadData()
    layoutIfNeeded()
    let newContentHeight = self.contentSize.height
    DispatchQueue.main.async {
        var contentOffset = self.contentOffset
        contentOffset.y += newContentHeight - currentContentHeight
        self.setContentOffset(contentOffset, animated: false)
    }
  }
}

use like this:

   self.reloadSomeData()
   collectionView.reloadDataAndKeepContentOffsetInPlace { [weak self] in
                                guard let self = self else { return }
                                self.collectionView.reloadData()
                            }

Upvotes: 0

Hitesh Surani
Hitesh Surani

Reputation: 13537

Just set estimatedRowHeight to maximum possible value.

 self.tableView.estimatedRowHeight = 1000
 self.tableView.estimatedSectionFooterHeight = 100.0
 self.tableView.estimatedSectionHeaderHeight = 500.0

That's it!!

Note:

Please do not use FLT_MAX, DBL_MAX value. May be it will crash your app.

Upvotes: 11

Pankaj Kulkarni
Pankaj Kulkarni

Reputation: 561

In iOS 12.x, using Xcode 10.2.1, an easier option is.

UIView.performWithoutAnimation { 
    let loc = tableView.contentOffset
    tableView.reloadRows(at: [indexPath], with: .none)
    tableView.contentOffset = loc
}

This works better than following; it shakes at times when the row is not fully visible.

let contentOffset = self.tableView.contentOffset
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
self.tableView.setContentOffset(contentOffset, animated: false)

Upvotes: 4

Zafar Ahmad
Zafar Ahmad

Reputation: 3219

Swift 4.2 : Simple Solution

override func viewDidLoad() {
 super.viewDidLoad()

 self.tableView.estimatedRowHeight = 0
 self.tableView.estimatedSectionHeaderHeight = 0
 self.tableView.estimatedSectionFooterHeight = 0
}

//And then simply update(insert, reloadSections, delete etc) your tableView or reload

tableView.reloadData()

//or

UIView.performWithoutAnimation {

  tableView.beginUpdates()
  .....
  tableView.endUpdates()
}

Upvotes: 2

Yifan
Yifan

Reputation: 1245

Use Extension

create UITableViewExtensions.swift and add following:

extension UITableView {

    func reloadDataWithoutScroll() {
        let offset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(offset, animated: false)
    }
}

Upvotes: 5

Trung Phan
Trung Phan

Reputation: 923

SWIFT 3

let contentOffset = self.tableView.contentOffset
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
self.tableView.setContentOffset(contentOffset, animated: false)

This is error of iOS8 when using UITableViewAutomatic Dimension. We need store the content offset of table, reload table, force layout and set contenOffset back.

CGPoint contentOffset = self.tableView.contentOffset;
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
[self.tableView setContentOffset:contentOffset];

Upvotes: 27

Apisteftos
Apisteftos

Reputation: 79

When you want to reload you have to

self.tableView.reloadData()

self.tableView.layoutIfNeeded()

and also use this UITableViewDelegate

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 'your maximum cell's height'
}

and your tableView will remain on the previous scroll position without scrolling

Upvotes: 1

JhanviR
JhanviR

Reputation: 31

This code will prevent unnecessary animation and maintain the scroll view's content offset, it worked fine for me.

let lastScrollOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.reloadData()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastScrollOffset, animated: false)

Upvotes: 1

Jack Chen
Jack Chen

Reputation: 1

try to replace

reloadData with

tableView.reloadRows(at: tableView!.indexPathsForVisibleRows!, with: .none),

but you should be care about no cells, if no cells, this method should cause crash.

Upvotes: 0

David Seek
David Seek

Reputation: 17132

For Swift 3+:

You need to save the current offset of the UITableView, then reload and then set the offset back on the UITableView.

I have created this function for this purpose:

func reload(tableView: UITableView) {

    let contentOffset = tableView.contentOffset
    tableView.reloadData()
    tableView.layoutIfNeeded()
    tableView.setContentOffset(contentOffset, animated: false)

}

Simply call it with: reload(tableView: self.tableView)

Upvotes: 55

Marco M
Marco M

Reputation: 1235

I'm doing it this way:

messages.insertContentsOf(incomingMsgs.reverse(), at: 0)
table.reloadData()

// This is for the first load, first 20 messages, scroll to bottom
if (messages.count <= 20) {
      let indexToScroll = NSIndexPath(forRow: saferSelf.messages.count - 1, inSection: 0)
      table.scrollToRowAtIndexPath(indexToScroll, atScrollPosition: .Top , animated: false)
}
// This is to reload older messages on top of tableview
else {
      let indexToScroll = NSIndexPath(forRow: incomingMsgs.count, inSection: 0)
      table.scrollToRowAtIndexPath(indexToScroll, atScrollPosition: .Top , animated: false)
      // Remove the refreshControl.height + tableHeader.height from the offset so the content remain where it was before reload
      let theRightOffset = CGPointMake(0, table.contentOffset.y - refreshControl.frame.height - table.headeView.frame.height)
      table.setContentOffset(theRightOffset, animated: false)
}

...also, since I use dynamic cell height, to avoid some weirdness, the estimation is cached:

var heightAtIndexPath = [NSIndexPath: CGFloat]()
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return heightAtIndexPath[indexPath] ?? UITableViewAutomaticDimension
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    heightAtIndexPath[indexPath] = cell.frame.height
}

Upvotes: 4

Prasad
Prasad

Reputation: 6034

I am showing if only one row is being added. You can extend it to multiple rows.

    // dataArray is your data Source object
    [dataArray insertObject:name atIndex:0];
    CGPoint contentOffset = self.tableView.contentOffset;
    contentOffset.y += [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
    [self.tableView reloadData];
    [self.tableView setContentOffset:contentOffset];

But for this to work you need to have defined - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath the method. Or else, you can directly give your tableview row height if it is constant.

Upvotes: 11

Related Questions