ios UITableView 像 Facebook 应用程序一样滚动到底部时加载更多

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20269474/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 22:32:51  来源:igfitidea点击:

UITableView load more when scrolling to bottom like Facebook application

iosiphoneuitableviewpagination

提问by rokridi

I am developing an application that uses SQLite. I want to show a list of users (UITableView) using a paginating mechanism. Could any one please tell me how to load more data in my list when the user scrolls to the end of the list (like on home page on Facebook application)?

我正在开发一个使用 SQLite 的应用程序。我想使用分页机制显示用户列表(UITableView)。任何人都可以告诉我当用户滚动到列表末尾时如何在我的列表中加载更多数据(例如在 Facebook 应用程序的主页上)?

回答by shinyuX

You can do that by adding a check on where you're at in the cellForRowAtIndexPath:method. This method is easy to understand and to implement :

您可以通过添加检查您在cellForRowAtIndexPath:方法中的位置来做到这一点。这种方法很容易理解和实现:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Classic start method
    static NSString *cellIdentifier = @"MyCell";
    MyCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell)
    {
        cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MainMenuCellIdentifier];
    }

    MyData *data = [self.dataArray objectAtIndex:indexPath.row];
    // Do your cell customisation
    // cell.titleLabel.text = data.title;

    BOOL lastItemReached = [data isEqual:[[self.dataArray] lastObject]]; 
    if (!lastItemReached && indexPath.row == [self.dataArray count] - 1)
    {
        [self launchReload];
    }
}

EDIT : added a check on last item to prevent recursion calls. You'll have to implement the method defining whether the last item has been reached or not.

编辑:添加了对最后一项的检查以防止递归调用。您必须实现定义是否已到达最后一项的方法。

EDIT2 : explained lastItemReached

EDIT2 :解释 lastItemReached

回答by Suragch

Swift

迅速

Method 1: Did scroll to bottom

方法一:滚动到底部

Here is the Swift version of Pedro Rom?o's answer. When the user stops scrolling it checks if it has reached the bottom.

这是Pedro Rom?o 答案的 Swift 版本。当用户停止滚动时,它会检查它是否已到达底部。

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {

    // UITableView only moves in one direction, y axis
    let currentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height

    // Change 10.0 to adjust the distance from bottom
    if maximumOffset - currentOffset <= 10.0 {
        self.loadMore()
    }
}

Method 2: Reached last row

方法二:到达最后一行

And here is the Swift version of shinyuX's answer. It checks if the user has reached the last row.

这是ShinyuX 答案的 Swift 版本。它检查用户是否已到达最后一行。

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    // set up cell
    // ...

    // Check if the last row number is the same as the last current data element
    if indexPath.row == self.dataArray.count - 1 {
        self.loadMore()
    }

}

Example of a loadMore()method

loadMore()方法示例

I set up these three class variables for fetching batches of data.

我设置了这三个类变量来获取批量数据。

// number of items to be fetched each time (i.e., database LIMIT)
let itemsPerBatch = 50

// Where to start fetching items (database OFFSET)
var offset = 0

// a flag for when all database items have already been loaded
var reachedEndOfItems = false

This is the function to load more items from the database into the table view.

这是将更多项目从数据库加载到表视图中的功能。

func loadMore() {

    // don't bother doing another db query if already have everything
    guard !self.reachedEndOfItems else {
        return
    }

    // query the db on a background thread
    DispatchQueue.global(qos: .background).async {

        // determine the range of data items to fetch
        var thisBatchOfItems: [MyObjects]?
        let start = self.offset
        let end = self.offset + self.itemsPerBatch

        // query the database
        do {
            // SQLite.swift wrapper
            thisBatchOfItems = try MyDataHelper.findRange(start..<end)
        } catch _ {
            print("query failed")
        }

        // update UITableView with new batch of items on main thread after query finishes
        DispatchQueue.main.async {

            if let newItems = thisBatchOfItems {

                // append the new items to the data source for the table view
                self.myObjectArray.appendContentsOf(newItems)

                // reload the table view
                self.tableView.reloadData()

                // check if this was the last of the data
                if newItems.count < self.itemsPerBatch {
                    self.reachedEndOfItems = true
                    print("reached end of data. Batch count: \(newItems.count)")
                }

                // reset the offset for the next data query
                self.offset += self.itemsPerBatch
            }

        }
    }
}

回答by Suraj Mirajkar

Better to use willDisplayCellmethod to check if which cell will be loaded. Once we get the current indexPath.rowis last we can load more cells. This will load more cells on scrolling down.

最好使用willDisplayCell方法来检查是否将加载哪个单元格。一旦我们获得了最新的电流indexPath.row,我们就可以加载更多的单元格。这将在向下滚动时加载更多单元格。

 - (void)tableView:(UITableView *)tableView 
       willDisplayCell:(UITableViewCell *)cell    
       forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // check if indexPath.row is last row
    // Perform operation to load new Cell's.
}

回答by Vasily Bodnarchuk

Details

细节

  • Swift 5.1, Xcode 11.2.1
  • 斯威夫特 5.1,Xcode 11.2.1

Solution

解决方案

Worked with UIScrollView / UICollectionView / UITableView

使用 UIScrollView / UICollectionView / UITableView

import UIKit

class LoadMoreActivityIndicator {

    private let spacingFromLastCell: CGFloat
    private let spacingFromLastCellWhenLoadMoreActionStart: CGFloat
    private weak var activityIndicatorView: UIActivityIndicatorView?
    private weak var scrollView: UIScrollView?

    private var defaultY: CGFloat {
        guard let height = scrollView?.contentSize.height else { return 0.0 }
        return height + spacingFromLastCell
    }

    deinit { activityIndicatorView?.removeFromSuperview() }

    init (scrollView: UIScrollView, spacingFromLastCell: CGFloat, spacingFromLastCellWhenLoadMoreActionStart: CGFloat) {
        self.scrollView = scrollView
        self.spacingFromLastCell = spacingFromLastCell
        self.spacingFromLastCellWhenLoadMoreActionStart = spacingFromLastCellWhenLoadMoreActionStart
        let size:CGFloat = 40
        let frame = CGRect(x: (scrollView.frame.width-size)/2, y: scrollView.contentSize.height + spacingFromLastCell, width: size, height: size)
        let activityIndicatorView = UIActivityIndicatorView(frame: frame)
        activityIndicatorView.color = .black
        activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
        activityIndicatorView.hidesWhenStopped = true
        scrollView.addSubview(activityIndicatorView)
        self.activityIndicatorView = activityIndicatorView
    }

    private var isHidden: Bool {
        guard let scrollView = scrollView else { return true }
        return scrollView.contentSize.height < scrollView.frame.size.height
    }

    func start(closure: (() -> Void)?) {
        guard let scrollView = scrollView, let activityIndicatorView = activityIndicatorView else { return }
        let offsetY = scrollView.contentOffset.y
        activityIndicatorView.isHidden = isHidden
        if !isHidden && offsetY >= 0 {
            let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
            let offsetDelta = offsetY - contentDelta

            let newY = defaultY-offsetDelta
            if newY < scrollView.frame.height {
                activityIndicatorView.frame.origin.y = newY
            } else {
                if activityIndicatorView.frame.origin.y != defaultY {
                    activityIndicatorView.frame.origin.y = defaultY
                }
            }

            if !activityIndicatorView.isAnimating {
                if offsetY > contentDelta && offsetDelta >= spacingFromLastCellWhenLoadMoreActionStart && !activityIndicatorView.isAnimating {
                    activityIndicatorView.startAnimating()
                    closure?()
                }
            }

            if scrollView.isDecelerating {
                if activityIndicatorView.isAnimating && scrollView.contentInset.bottom == 0 {
                    UIView.animate(withDuration: 0.3) { [weak self] in
                        if let bottom = self?.spacingFromLastCellWhenLoadMoreActionStart {
                            scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0)
                        }
                    }
                }
            }
        }
    }

    func stop(completion: (() -> Void)? = nil) {
        guard let scrollView = scrollView , let activityIndicatorView = activityIndicatorView else { return }
        let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
        let offsetDelta = scrollView.contentOffset.y - contentDelta
        if offsetDelta >= 0 {
            UIView.animate(withDuration: 0.3, animations: {
                scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
            }) { _ in completion?() }
        } else {
            scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
            completion?()
        }
        activityIndicatorView.stopAnimating()
    }
}

Usage

用法

init

在里面

activityIndicator = LoadMoreActivityIndicator(scrollView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)

handling

处理

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        activityIndicator.start {
            DispatchQueue.global(qos: .utility).async {
                sleep(3)
                DispatchQueue.main.async { [weak self] in
                    self?.activityIndicator.stop()
                }
            }
        }
    }
}

Full Sample

完整样本

Do not forget to paste the solution code.

不要忘记粘贴解决方案代码。

import UIKit

class ViewController: UIViewController {

    fileprivate var activityIndicator: LoadMoreActivityIndicator!

    override func viewDidLoad() {
        super.viewDidLoad()
        let tableView = UITableView(frame: view.frame)
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

        tableView.dataSource = self
        tableView.delegate = self
        tableView.tableFooterView = UIView()
        activityIndicator = LoadMoreActivityIndicator(scrollView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)
    }
}

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 30
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = "\(indexPath)"
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        activityIndicator.start {
            DispatchQueue.global(qos: .utility).async {
                for i in 0..<3 {
                    print("!!!!!!!!! \(i)")
                    sleep(1)
                }
                DispatchQueue.main.async { [weak self] in
                    self?.activityIndicator.stop()
                }
            }
        }
    }
}

Result

结果

enter image description here

在此处输入图片说明

回答by samwize

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
    NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
    if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
        // This is the last cell
        [self loadMore];
    }
}

If you are using Core Data and NSFetchedResultsController, then loadMorecould look like the following:

如果您使用的是 Core Data 和NSFetchedResultsController,则loadMore可能如下所示:

// Load more
- (void)loadMore {
    [self.fetchedResultsController.fetchRequest setFetchLimit:newFetchLimit];
    [NSFetchedResultsController deleteCacheWithName:@"cache name"];
    NSError *error;
    if (![self.fetchedResultsController performFetch:&error]) {
        // Update to handle the error appropriately.
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }

    [self.tableView reloadData];
}

回答by Pedro Rom?o

I have implemented one solution that i found in stackoverflow, and it works fine, but i think the shinyuX's solution it's very easy to implement and works fine for my propose. If someone wants a different solution can use this one below.

我已经实现了我在 stackoverflow 中找到的一个解决方案,它运行良好,但我认为 ShinyuX 的解决方案很容易实现并且适用于我的建议。如果有人想要不同的解决方案,可以使用下面的解决方案。

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{

   // UITableView only moves in one direction, y axis
    CGFloat currentOffset = scrollView.contentOffset.y;
    CGFloat maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    //NSInteger result = maximumOffset - currentOffset;

    // Change 10.0 to adjust the distance from bottom
    if (maximumOffset - currentOffset <= 10.0) {
        [self loadOneMorePage];
        //[self methodThatAddsDataAndReloadsTableView];
    }
}

回答by Retterdesdialogs

Use limit and offset in your queries and fill your tableview with that content. When the user scrolls down, load the next offset.

在您的查询中使用 limit 和 offset 并用该内容填充您的 tableview。当用户向下滚动时,加载下一个偏移量。

Implement the tableView:willDisplayCell:forRowAtIndexPath:method in your UITableViewDelegateand check to see if it's the last row

tableView:willDisplayCell:forRowAtIndexPath:在你的方法中实现UITableViewDelegate并检查它是否是最后一行

回答by Yogi

Below link will provide sample code. #Swift3

下面的链接将提供示例代码。#Swift3

User need to pull up last table view cell, at least hight of 2 cell to fetch more data from server.

用户需要拉起最后一个表格视图单元格,至少 2 个单元格的高度才能从服务器获取更多数据。

You will found Process cell also to show loading process as in last cell.

您还会发现 Process 单元格也显示了最后一个单元格中的加载过程。

Its in Swift3

它在 Swift3 中

https://github.com/yogendrabagoriya/YBTableViewPullData

https://github.com/yogendrabagoriya/YBTableViewPullData

回答by Vitalii

One more option to use (Swift 3and iOS 10+):

另一个使用选项(Swift 3和 iOS 10+):

class DocumentEventsTableViewController: UITableViewController, UITableViewDataSourcePrefetching {

     var currentPage: Int = 1
     let pageSize: Int = 10 // num of items in one page

     override func viewDidLoad() {
         super.viewDidLoad()

         self.tableView.prefetchDataSource = self
     }

     func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
         let upcomingRows = indexPaths.map { 
extension UITableView{

    func indicatorView() -> UIActivityIndicatorView{
        var activityIndicatorView = UIActivityIndicatorView()
        if self.tableFooterView == nil{
            let indicatorFrame = CGRect(x: 0, y: 0, width: self.bounds.width, height: 40)
            activityIndicatorView = UIActivityIndicatorView(frame: indicatorFrame)
            activityIndicatorView.isHidden = false
            activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
            activityIndicatorView.isHidden = true
            self.tableFooterView = activityIndicatorView
            return activityIndicatorView
        }else{
            return activityIndicatorView
        }
    }

    func addLoading(_ indexPath:IndexPath, closure: @escaping (() -> Void)){
        indicatorView().startAnimating()
        if let lastVisibleIndexPath = self.indexPathsForVisibleRows?.last {
            if indexPath == lastVisibleIndexPath && indexPath.row == self.numberOfRows(inSection: 0) - 1 {
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    closure()
                }
            }
        }
        indicatorView().isHidden = false
    }

    func stopLoading(){
        indicatorView().stopAnimating()
        indicatorView().isHidden = true
    }
}
.row } if let maxIndex = upcomingRows.max() { let nextPage: Int = Int(ceil(Double(maxIndex) / Double(pageSize))) + 1 if nextPage > currentPage { // Your function, which attempts to load respective page from the local database loadLocalData(page: nextPage) // Your function, which makes a network request to fetch the respective page of data from the network startLoadingDataFromNetwork(page: nextPage) currentPage = nextPage } } } }

For rather small pages (~ 10 items) you might want to manually add data for pages 1 and 2 because nextPage might be somewhere about 1-2 until the table has a few items to be scrolled well. But it will work great for all next pages.

对于相当小的页面(约 10 个项目),您可能需要手动添加第 1 页和第 2 页的数据,因为 nextPage 可能在大约 1-2 的某个位置,直到表格有几个项目需要滚动。但它适用于所有下一页。

回答by Yogesh Patel

Details

细节

  • Swift 5.1, Xcode 11.3.1
  • 斯威夫特 5.1,Xcode 11.3.1

Solution

解决方案

Genetic UITableView Extension For Loadmore.

Loadmore 的遗传 UITableView 扩展。

add this UITableView + Extension in your new file

在你的新文件中添加这个 UITableView + Extension

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    // need to pass your indexpath then it showing your indicator at bottom 
    tableView.addLoading(indexPath) {
        // add your code here
        // append Your array and reload your tableview
        tableView.stopLoading() // stop your indicator
    }
}

Now, just add following line of code in UITableViewDelegate Method willDisplay Cell in your ViewController and make sure tableView.delegate = self

现在,只需在 UITableViewDelegate 方法 willDisplay Cell 在您的 ViewController 中添加以下代码行并确保 tableView.delegate = self

##代码##

Result

结果

enter image description here

在此处输入图片说明

That's it.. Hope this helpful. Thank You

就是这样..希望这有帮助。谢谢你