当Facebook应用程序滚动到底部时,UITableView加载更多

rok*_*idi 88 iphone pagination uitableview ios

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

shi*_*yuX 102

您可以通过添加检查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];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

EDIT2:解释了lastItemReached

  • 如果用户上下滚动怎么办,所以cellForRowAtIndexPath被称为MANY TIMES!?? (8认同)
  • 我必须添加一个标志来防止在最后一个项目被命中时出现递归问题:`if!lastItemReached && indexPath.row == dataArray!.hits.count - 1 {` (4认同)

Sur*_*gch 55

迅速

方法1:滚动到底部

这是Swro版本的PedroRomão的答案.当用户停止滚动时,它会检查它是否已到达底部.

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

方法2:到达最后一行

这是swinguX的答案的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()
    }

}
Run Code Online (Sandbox Code Playgroud)

loadMore()方法示例

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

// 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
Run Code Online (Sandbox Code Playgroud)

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

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
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Sur*_*kar 36

最好使用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.
}
Run Code Online (Sandbox Code Playgroud)

  • 它没有更好,因为reloadData会再次调用此方法吗? (15认同)

Vas*_*huk 18

细节

Xcode 10,Swift 4.2

使用UIScrollView/UICollectionView/UITableView

import UIKit

protocol LoadMoreControlDelegate: class {
    func loadMoreControl(didStartAnimating loadMoreControl: LoadMoreControl)
    func loadMoreControl(didStopAnimating loadMoreControl: LoadMoreControl)
}

class LoadMoreControl {

    private let spacingFromLastCell: CGFloat
    private let indicatorHeight: CGFloat
    private weak var activityIndicatorView: UIActivityIndicatorView?
    private weak var scrollView: UIScrollView?
    weak var delegate: LoadMoreControlDelegate?

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

    init (scrollView: UIScrollView, spacingFromLastCell: CGFloat, indicatorHeight: CGFloat) {
        self.scrollView = scrollView
        self.spacingFromLastCell = spacingFromLastCell
        self.indicatorHeight = indicatorHeight

        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.hidesWhenStopped = true
        activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
        scrollView.addSubview(activityIndicatorView)
        activityIndicatorView.isHidden = isHidden
        self.activityIndicatorView = activityIndicatorView
    }

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

    func didScroll() {
        guard let scrollView = scrollView, let activityIndicatorView = activityIndicatorView else { return }
        let offsetY = scrollView.contentOffset.y
        activityIndicatorView.isHidden = isHidden
        if !activityIndicatorView.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 >= indicatorHeight && !activityIndicatorView.isAnimating {
                    activityIndicatorView.startAnimating()
                    delegate?.loadMoreControl(didStartAnimating: self)
                }
            }

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

    func stop() {
        guard let scrollView = scrollView 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: { [weak scrollView] in
                scrollView?.contentInset = .zero
            }) { [weak self] result in
                if result { self?.endAnimating() }
            }
        } else {
            scrollView.contentInset = .zero
            endAnimating()
        }
    }

    private func endAnimating() {
        activityIndicatorView?.stopAnimating()
        delegate?.loadMoreControl(didStopAnimating: self)
    }
}
Run Code Online (Sandbox Code Playgroud)

用法

loadMoreControl = LoadMoreControl(scrollView: tableView, spacingFromLastCell: 10, indicatorHeight: 60)
loadMoreControl.delegate = self
Run Code Online (Sandbox Code Playgroud)

完整样本

extension ViewController: LoadMoreControlDelegate {
    func loadMoreControl(didStartAnimating loadMoreControl: LoadMoreControl) {
        print("didStartAnimating")
    }

    func loadMoreControl(didStopAnimating loadMoreControl: LoadMoreControl) {
        print("didStopAnimating")
    }
}

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        loadMoreControl.didScroll()
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

在此输入图像描述


sam*_*ize 17

- (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];
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您使用的是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];
}
Run Code Online (Sandbox Code Playgroud)


Yog*_*tel 12

细节

  • 斯威夫特 5.1,Xcode 11.3.1

解决方案

Loadmore 的遗传 UITableView 扩展。

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

extension UITableView {

func indicatorView() -> UIActivityIndicatorView{
    var activityIndicatorView = UIActivityIndicatorView()
    if self.tableFooterView == nil {
        let indicatorFrame = CGRect(x: 0, y: 0, width: self.bounds.width, height: 80)
        activityIndicatorView = UIActivityIndicatorView(frame: indicatorFrame)
        activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
        
        if #available(iOS 13.0, *) {
            activityIndicatorView.style = .large
        } else {
            // Fallback on earlier versions
            activityIndicatorView.style = .whiteLarge
        }
        
        activityIndicatorView.color = UIColor().ThemeColor
        activityIndicatorView.hidesWhenStopped = 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()
            }
        }
    }
}

func stopLoading() {
    if self.tableFooterView != nil {
        self.indicatorView().stopAnimating()
        self.tableFooterView = nil
    }
    else {
        self.tableFooterView = nil
    }
} 
}
Run Code Online (Sandbox Code Playgroud)

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

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
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

在此处输入图片说明

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


Ped*_*mão 11

我已经实现了一个我在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];
    }
}
Run Code Online (Sandbox Code Playgroud)


Ret*_*ogs 6

在查询中使用限制和偏移量,并使用该内容填充tableview.当用户向下滚动时,加载下一个偏移量.

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


小智 5

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

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

您还会发现Process cell也显示加载过程,如同最后一个单元格一样.

它在Swift3中

https://github.com/yogendrabagoriya/YBTableViewPullData


Vit*_*lii 5

另一种可供使用的选项(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 { $0.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
             }
         }
     }
 }
Run Code Online (Sandbox Code Playgroud)

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