0

I have a UITableView that implements a type of 'infinite scrolling'.

This is done by calculating the IndexPath of the new data and then passing to tableView.insertRows(at: indexPaths, with: .none).

My data is returned from an api in pages of 50. What I am seeing however is when a user scrolls very quickly to the bottom, the table will insert rows and then jump to a section further up, essentially losing their place.

enter image description here

My table is created using this snippet

  private func addTableView() {
        tableView = UITableView(frame: .zero)
        tableView.showsVerticalScrollIndicator = false
        tableView.showsHorizontalScrollIndicator = false
        tableView.rowHeight = UITableView.automaticDimension
        tableView.bounces = true
        tableView.estimatedRowHeight = 200
        tableView.separatorStyle = .none
        tableView.isHidden = true
        tableView.backgroundColor = .clear
        tableView.tableFooterView = UIView()

        addSubview(tableView)
        tableView.position(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.reuseID)
    }

And the reload is triggered using

  func insertRows(_ indexPaths: [IndexPath]) {
        UIView.performWithoutAnimation {
            tableView.insertRows(at: indexPaths, with: .none)
            self.tableView.isHidden = false
        }
    }

I am using performWithoutAnimation as I did not like any of the animations for inserting rows.

In my view model I inject a FeedTableViewProvider conforming to UITableViewDataSource, UITableViewDelegate and has the following methods

protocol FeedTableViewProviderType: class {
    var data: Feed? { get set }
    var feed: [FeedItem] { get }
    var insertRows: (([IndexPath]) -> Void)? { get set }
    var didRequestMoreData: ((Int) -> Void)? { get set }
}

class FeedTableViewProvider: NSObject, FeedTableViewProviderType {

    var insertRows: (([IndexPath]) -> Void)?
    var didRequestMoreData: ((Int) -> Void)?

    var data: Feed? {
        didSet {
            guard let data = data else { return }
            self.addMoreRows(data.feed)
        }
    }

    private(set) var feed = [FeedItem]() {
        didSet {
            isPaginating = false
        }
    }

    private var isPaginating = false

    private func addMoreRows(_ data: [FeedItem]) {
        var indexPaths = [IndexPath]()

        data.indices.forEach { indexPaths.append(IndexPath(row: feed.count + $0, section: 0)) }

        feed.append(contentsOf: data.sorted(by: { $0.props.createdDate > $1.props.createdDate }))

        insertRows?(indexPaths)
    }

    private func requestNextPage() {
        guard let currentPage = data?.currentPage, let totalPages = data?.totalPages, currentPage < totalPages else { return }
        didRequestMoreData?(currentPage + 1)
    }
}

extension FeedTableViewProvider: TableViewProvider {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return feed.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: UITableViewCell.reuseID, for: indexPath)
        cell.textLabel?.text = "Cell # \(indexPath.row)"
        return cell
    }

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if indexPath.item == feed.count - 1 && !isPaginating {
            isPaginating = true
            requestNextPage()
        }
    }
}
Tim J
  • 1,211
  • 1
  • 14
  • 31

3 Answers3

1

I suspect the cause of this is actually to do with using

tableView.rowHeight = UITableView.automaticDimension
....
tableView.estimatedRowHeight = 200

The position changes as the offset is changing when new cells are inserted.

I would start by keeping some sort of cache containing your cell heights

private var sizeCache: [IndexPath: CGFloat] = [IndexPath: CGFloat]()

You can then capture that as the cell is scrolled off screen

func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    sizeCache[indexPath] = cell.frame.size.height
}

Now make sure to apply that size from the cache

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return sizeCache[indexPath] ?? UITableView.automaticDimension
}

Now as cells are inserted and they jump with the new offset, they should render with their correct height, meaning the view should essentially stay on position.

nodediggity
  • 2,458
  • 1
  • 9
  • 12
0

use this code while adding new data into tableview.

  UIView.setAnimationsEnabled(false)
    self.tableView.beginUpdates()
    self.tableView.reloadSections(NSIndexSet(index: 0) as IndexSet, with: UITableViewRowAnimation.none)
    self.tableView.endUpdates()
Hiren Rathod
  • 99
  • 12
-1

You can use this function on every new row data insertion in Tableview to prevent scrolling to the bottom.

func scrollToTop(){
        DispatchQueue.main.async {
            let indexPath = IndexPath(row: self.dataArray.count-1, section: 0)
            self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
        }
    }
Azizul Hoq
  • 589
  • 1
  • 4
  • 13
  • I don't want it to scroll to the bottom or top, when new rows are added I want it keep the current position. – Tim J Jun 22 '19 at 06:24
  • https://stackoverflow.com/questions/33338726/reload-tableview-section-without-scroll-or-animation Use this link it might be helpful for your issue. @TimJ – Hiren Rathod Jun 22 '19 at 07:26
  • https://gist.github.com/WorldDownTown/9ed681148e9224f7157c9e8620ce46b8 -Then this link could serve your purpose. @Tim J – Azizul Hoq Jun 22 '19 at 07:34
  • Thanks for your input @AzizulHoq but I am trying to avoid calling `tableView.reloadData()` – Tim J Jun 22 '19 at 08:37