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

Posted

技术标签:

【中文标题】UITableView 在像 Facebook 应用程序一样滚动到底部时加载更多【英文标题】:UITableView load more when scrolling to bottom like Facebook application 【发布时间】:2013-12-14 16:32:05 【问题描述】:

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

【问题讨论】:

【参考方案1】:

您可以通过在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];
    

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

EDIT2:解释 lastItemReached

【讨论】:

如果用户上下滚动怎么办,所以 cellForRowAtIndexPath 被多次调用!?? 第一次滚动到底部时,他的列表将被重新加载。每次他触底时,都会收集新的数据块。如果必须应用任何特定处理,则由 launchReload 方法负责处理(例如,一次只能执行一个异步重新加载操作) 我必须添加一个标志以防止在最后一项被击中时出现递归问题:if !lastItemReached && indexPath.row == dataArray!.hits.count - 1 self.launchReload 方法是什么? @shinyuX 对我不起作用,“如果”总是错误的...但是如果 (lastItemReached && indexPath.row == [self.dataArray count] - 1) 为真,为什么?【参考方案2】:

斯威夫特

方法一:滚动到底部

这是Pedro Romão's answer 的 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()
    

方法2:到达最后一行

这里是shinyuX's answer 的 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()
    


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

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

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
            

        
    

【讨论】:

我使用了方法 1,因为我想拉取更多。它工作得很好。谢谢你们!【参考方案3】:

最好使用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.

【讨论】:

不是更好,因为 reloadData 会再次调用此方法对吗? 是的,这适用于部分,indexPath 将为您提供行和部分。【参考方案4】:

详情

Swift 5.1,Xcode 11.2.1

解决方案

使用 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)
        if #available(ios 13.0, *)
        
            activityIndicatorView.color = .label
        
        else
        
            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()
    

用法

初始化

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

处理

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()
                
            
        
    

完整样本

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

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()
                
            
        
    

结果

【讨论】:

完美运行。但是我的表格视图中有一个标题,在拖动以加载更多内容后,标题将位于导航栏下方.. loadMoreActionFinshed 中的 UIEdgeInsetsMake 应设置为 (62, 0, 0, 0) 考虑到 66 = navbar.height + 22 垂直滚动时它应该在 CollectionView 中工作。 难以置信...酷! 这个的任何objective-c版本? @VasilyBodnarchuk 没问题,我会做的,在这里分享给其他人【参考方案5】:
- (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];
    

如果您使用 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];

【讨论】:

我正在尝试实现这一点,但我使用的是结果数组而不是 sqlite,想知道如何在我拥有的当前 NSMutableArray 中添加更多内容,然后重新加载数据,否则数据被覆盖...我试过这个 [names addObjectsFromArray: [responseObject valueForKeyPath:@"name"]];但它不起作用......这是我的问题的链接***.com/questions/23446780/… 每次获取新数据时重新获取数据有什么意义?如果 frc 配置正确,单次 fetch 就足够了,它会根据需要进行相应的更新。每次都获取它,假设frc的获取请求配置到主线程上下文将阻塞主线程,因为它撞击磁盘,当用户想要新数据时,这完全不利于用户体验。 前半部分对我很有帮助,谢谢。 (不使用 FetchedResultsVC) @MANIAK_dobrii 是正确的。 NSFetchedResultsController 的关键特性之一是它计算分页数据,以便在将其连接到 UITableView 时免费获得虚拟滚动。仅当您实际上用更多数据填充 CoreData 存储时才需要实现这样的 loadMore 函数,在这种情况下,如果您的 NSFetchedResultsController 配置正确,则无需执行另一个 performFetch。 与其他答案相同的问题。 reloadData 导致这种情况发生多次。【参考方案6】:

详情

Swift 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 = .systemPink
        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
    
 

现在,只需在 UITableViewDelegate Method willDisplay Cell 中添加以下代码行并确保 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
    

结果

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

【讨论】:

需要考虑的事项。只需在 stoploading 函数中添加 'tableFooterView = nil' ,否则指示器旋转不会停止动画。 activityIndi​​cator 'hidesWhenStopped' 中还有一个属性,因此您无需手动设置隐藏的真/假指标。但总的来说它看起来很棒:) 感谢您的建议,我将检查一次并编辑此答案:-) 谢谢兄弟。这个答案比互联网上任何其他解决方案都要好。 只是几件事。 1)每次要显示单元格时都会调用“addLoading”。在检查 lastVisibleIndexPath 等之前,indicatorView 不应启动动画。 2)“addLoading”使用indicatorView,每次用UIActivityIndi​​catorView()创建。如果 tableViewFooter 为 nil,则您已经从那里创建了它。如果 tableViewFooter 不是 nil,你可以检查它是否真的是 UIActivityIndi​​catorView,而不是创建一个新的。【参考方案7】:

我已经实现了我在 *** 中找到的一种解决方案,它运行良好,但我认为 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];
    

【讨论】:

我认为视图呈现有不同的场景,在我的情况下你的解决方案有效,我需要这样的东西 如果用户用力一甩,即1.5屏幕高,可以到达底部而不触发刷新。 但它会将列表滚动到顶部【参考方案8】:

在您的查询中使用限制和偏移量,并用该内容填充您的表格视图。当用户向下滚动时,加载下一个偏移量。

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

【讨论】:

【参考方案9】:

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

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

您会发现 Process 单元格也显示加载过程,就像在最后一个单元格中一样。

它在 Swift3 中

https://github.com/yogendrabagoriya/YBTableViewPullData

【讨论】:

【参考方案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  $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
             
         
     
 

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

【讨论】:

这仅适用于只读数据。不起作用如果您具有删除某些行并加载更多的功能,因为 pageSize 已在此处修复,并且即使在更新源后有更多数据也无法加载更多。【参考方案11】:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 

    if (news.count == 0) 
        return 0;
     else 
        return news.count +  1 ;
    


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    @try 

        uint position = (uint) (indexPath.row);
        NSUInteger row = [indexPath row];
        NSUInteger count = [news count];

        //show Load More
        if (row == count) 
            UITableViewCell *cell = nil;

            static NSString *LoadMoreId = @"LoadMore";
            cell = [tableView dequeueReusableCellWithIdentifier:LoadMoreId];
            if (cell == nil) 
                cell = [[UITableViewCell alloc]
                        initWithStyle:UITableViewCellStyleDefault
                      reuseIdentifier:LoadMoreId];
            
            if (!hasMoreLoad) 
                cell.hidden = true;
             else 

                cell.textLabel.text = @"Load more items...";
                cell.textLabel.textColor = [UIColor blueColor];
                cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
                NSLog(@"Load more");
                if (!isMoreLoaded) 
                    isMoreLoaded = true;
                    [self performSelector:@selector(loadMoreNews) withObject:nil afterDelay:0.1];
                
            

            return cell;

         else 
            NewsRow *cell = nil;

            NewsObject *newsObject = news[position];
            static NSString *CellIdentifier = @"NewsRow";
            cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

            if (cell == nil) 
                // Load the top-level objects from the custom cell XIB.
                NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:CellIdentifier owner:self options:nil];
                // Grab a pointer to the first object (presumably the custom cell, as that's all the XIB should contain).
                cell = topLevelObjects[0];
                // Configure the cell...

            

            cell.title.text = newsObject.title;             
            return cell;
        

    
    @catch (NSException *exception) 
        NSLog(@"Exception occurred: %@, %@", exception, [exception userInfo]);
    
    return nil;

这篇文章的解释很好。

http://useyourloaf.com/blog/2010/10/02/dynamically-loading-new-rows-into-a-table.html

很简单,您必须添加最后一行并将其隐藏,当表格行到达最后一行时,而不是显示该行并加载更多项目。

【讨论】:

【参考方案12】:

你应该检查 ios UITableViewDataSourcePrefetching。

class ViewController: UIViewController 
    @IBOutlet weak var mytableview: UITableView!

    override func viewDidLoad() 
        super.viewDidLoad()
        mytableview.prefetchDataSource = self
    

 func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) 
        print("prefetchdRowsAtIndexpath \(indexPaths)")
    

    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) 
        print("cancelPrefetchingForRowsAtIndexpath \(indexPaths)")
    



https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching https://andreygordeev.com/2017/02/20/uitableview-prefetching/

【讨论】:

【参考方案13】:

对于 Xcode 10.1、Swift 4.2

This video 似乎是一个很棒的教程!

启动/完成项目:https://github.com/RobCanton/Swift-Infinite-Scrolling-Example

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate 

    var tableView:UITableView!

    var fetchingMore = false
    var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

    override func viewDidLoad() 
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        initTableView()
    

    func initTableView() 
        tableView = UITableView(frame: view.bounds, style: .plain)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
        tableView.delegate = self
        tableView.dataSource = self

        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false

        let layoutGuide = view.safeAreaLayoutGuide
        tableView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true

        tableView.reloadData()
    
    override func didReceiveMemoryWarning() 
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return items.count
    

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
            let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
            cell.textLabel?.text = "Item \(items[indexPath.row])"
            return cell
    

    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        let offsetY = scrollView.contentOffset.y
        let contentHeight = scrollView.contentSize.height

        if offsetY > contentHeight - scrollView.frame.height * 4 
            if !fetchingMore 
                beginBatchFetch()
            
        
    

    func beginBatchFetch() 
        fetchingMore = true
        print("Call API here..")
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.50, execute: 
            print("Consider this as API response.")
            let newItems = (self.items.count...self.items.count + 12).map  index in index 
            self.items.append(contentsOf: newItems)
            self.fetchingMore = false
            self.tableView.reloadData()
        )
    

【讨论】:

【参考方案14】:

用于从 API 加载,它适用于我,Xcode10,swift 4.2

1- 创建新的 Swift 文件并这样做:

//
//  apiTVCController.swift
//  ApiTestingTableView
//
//  Created by Hooma7n on 4/7/19.
//  Copyright © 2019 Hooma7n. All rights reserved.
//

import Foundation
import Alamofire

class apiget 

    var tableData : [Datum] = []
    var loadin : [Datum] = []
    var testfortotal : Int?


    func getfromapi(completionHandler : ((_ isSucess : Bool) -> Void)?) 
        let url = "https://reqres.in/api/users?page=1"
        Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
            .responseJSON(completionHandler :  response in
                switch response.result 
                case .success(let data):
                    guard let jsonData = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted) else return
                    let decoder = JSONDecoder()
                    guard let result = try? decoder.decode(Welcome.self, from: jsonData) else return
                    self.tableData = result.data ?? []
                    self.testfortotal = result.total ?? 0
                    completionHandler?(true)

                //                    print(result)
                case .failure(let error):
                    print(error)
                
            )
    

    var pagecounter : Int = 2


    func loadmore(completionHandler : ((_ isSucess : Bool) -> Void)?)

        let url = "https://reqres.in/api/users?page=\(pagecounter)"
        Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
            .responseJSON(completionHandler :  response in
                switch response.result 
                case .success(let data):
                    guard let jsonData = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted) else return
                    let decoder = JSONDecoder()
                    guard let myresult = try? decoder.decode(Welcome.self, from: jsonData) else return
                    self.loadin = myresult.data ?? []
                    self.tableData.append(contentsOf: myresult.data ?? [])
                    completionHandler?(true)
                    print(self.pagecounter)
                    self.pagecounter += 1

                //                    print(myresult)
                case .failure(let error):
                    print(error)
                
            )

    



extension apiget 

    struct Welcome: Codable 
        let page, perPage, total, totalPages: Int?
        var data: [Datum]?

        enum CodingKeys: String, CodingKey 
            case page
            case perPage = "per_page"
            case total
            case totalPages = "total_pages"
            case data
        
    

    struct Datum: Codable 
        let id: Int?
        let firstName, lastName: String?
        let avatar: String?

        enum CodingKeys: String, CodingKey 
            case id
            case firstName = "first_name"
            case lastName = "last_name"
            case avatar
        
    



2- 在您的 ViewController 文件(tableView 控制器)中:

//
//  apiTVC.swift
//  ApiTestingTableView
//
//  Created by Hooma7n on 4/7/19.
//  Copyright © 2019 Hooma7n. All rights reserved.
//

import UIKit
import Alamofire

class apiTVC: UITableViewController 

    var datamodel = apiget()

    override func viewDidLoad() 
        super.viewDidLoad()

        datamodel.getfromapi(completionHandler: finish in
            if finish self.tableView.reloadData()
            

        )

    


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

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        return datamodel.tableData.count
    

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 

        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! apiTableViewCell
        cell.firstNameLabel.text = datamodel.tableData[indexPath.row].firstName
        cell.lastNameLabel.text = datamodel.tableData[indexPath.row].lastName
        cell.dateLabel.text = "\(datamodel.tableData[indexPath.row].id ?? 0)"
        cell.profileImageView.loadImage(fromURL: datamodel.tableData[indexPath.row].avatar ?? "")

        return cell

    

    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
        let lastElement = datamodel.tableData.count - 1
        let total = datamodel.testfortotal ?? 12
        if indexPath.row == lastElement && datamodel.tableData.count < total

            datamodel.loadmore(completionHandler: finish in
                if finish 

                    self.tableView.reloadData()

                )
        
    

如果在你的 viewController 中使用 tableView 设置 delegate,datasource self in viewDidLoad.

【讨论】:

【参考方案15】:

只想分享这种方法:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

    NSLog(@"%@", [[YourTableView indexPathsForVisibleRows] lastObject]);
    [self estimatedTotalData];


- (void)estimatedTotalData

    long currentRow = ((NSIndexPath *)[[YourTableView indexPathsForVisibleRows] lastObject]).row;

    long estimateDataCount = 25;

    while (currentRow > estimateDataCount)
    
        estimateDataCount+=25;
    

    dataLimit = estimateDataCount;

    if (dataLimit == currentRow+1)
    
        dataLimit+=25;
    

    NSLog(@"dataLimit :%ld", dataLimit);

    [self requestForData];

    // this answers the question..
    //
    if(YourDataSource.count-1 == currentRow)
    
        NSLog(@"LAST ROW"); //loadMore data
    

NSLog(...); 输出类似于:

<NSIndexPath: 0xc0000000002e0016> length = 2, path = 0 - 92
dataLimit :100
<NSIndexPath: 0xc000000000298016> length = 2, path = 0 - 83
dataLimit :100
<NSIndexPath: 0xc000000000278016> length = 2, path = 0 - 79
dataLimit :100
<NSIndexPath: 0xc000000000238016> length = 2, path = 0 - 71
dataLimit :75
<NSIndexPath: 0xc0000000001d8016> length = 2, path = 0 - 59
dataLimit :75
<NSIndexPath: 0xc0000000001c0016> length = 2, path = 0 - 56
dataLimit :75
<NSIndexPath: 0xc000000000138016> length = 2, path = 0 - 39
dataLimit :50
<NSIndexPath: 0xc000000000120016> length = 2, path = 0 - 36
dataLimit :50
<NSIndexPath: 0xc000000000008016> length = 2, path = 0 - 1
dataLimit :25
<NSIndexPath: 0xc000000000008016> length = 2, path = 0 - 1
dataLimit :25

这对于显示本地存储的数据很有用。 最初我将 dataLimit 声明为 25,这意味着 uitableview 将有 0-24(最初)。

如果用户滚动到底部并且最后一个单元格可见,dataLimit 将添加 25...

注意:这更像是一个 UITableView 数据分页,:)

【讨论】:

【参考方案16】:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 

NSInteger sectionsAmount = [tableView numberOfSections];
NSInteger rowsAmount = [tableView numberOfRowsInSection:[indexPath section]];
if ([indexPath section] == sectionsAmount - 1 && [indexPath row] == rowsAmount - 1) 
    //get last row
    if (!isSearchActive && !isFilterSearchActive) 
        if (totalRecords % 8 == 0) 
            int64_t delayInSeconds = 2.0;
            dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
            dispatch_after(popTime, dispatch_get_main_queue(), ^(void) 


            [yourTableView beginUpdates];
            [yourTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
            [yourTableView endUpdates];
            );
        
    


【讨论】:

在显示最后一行之后,插入行,即 beginUpdates..并使用一些延迟来避免崩溃。【参考方案17】:

解决此问题的最佳方法是在表格底部添加单元格,该单元格将包含指示器。

在swift中你需要添加这个:

    创建 cellLoading 类型的新单元格,这将保存指标。看下面的代码 查看行数并加1(这是用于加载单元格)。 如果 idexPath.row == yourArray.count 则需要检查 rawAtIndex,然后返回加载单元格。

看看下面的代码:

import UIKit

class LoadingCell: UITableViewCell 

@IBOutlet weak var indicator: UIActivityIndicatorView!



对于表格视图: numOfRows:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
    return  yourArray.count + 1

cellForRawAt 索引路径:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 

    if indexPath.row == users.count  
        // need to change
        let loading = Bundle.main.loadNibNamed("LoadingCell", owner: LoadingCell.self , options: nil)?.first as! LoadingCell
        return loading

    

    let yourCell = tableView.dequeueReusableCell(withIdentifier: "cellCustomizing", for: indexPath) as! UITableViewCell

    return yourCell


如果您注意到我的加载单元是从 nib 文件创建的。 This videos 会解释我做了什么。

【讨论】:

【参考方案18】:
let threshold = 100.0 // threshold from bottom of tableView
var isLoadingMore = false // flag


func scrollViewDidScroll(scrollView: UIScrollView) 
    let contentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    if !isLoadingMore && (maximumOffset - contentOffset <= threshold) 
        // Get more data - API call
        self.isLoadingMore = true

        // Update UI
        dispatch_async(dispatch_get_main_queue()) 
            tableView.reloadData()
            self.isLoadingMore = false
        
    
  

【讨论】:

【参考方案19】:

这是示例代码。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
    let cell:ShowComplainCell = tableView.dequeueReusableCell(withIdentifier: "cell")! as! ShowComplainCell

    let item  = self.dataArray[indexPath.row] as! ComplainListItem;

    let indexPathArray = NSArray(array: tableView.indexPathsForVisibleRows!)
    let vIndexPath = indexPathArray.lastObject as! NSIndexPath

    let lastItemReached = item.isEqual(self.dataArray.lastObject);

    if (lastItemReached && vIndexPath.row == (self.dataArray.count - 1))
       

        self.loadData()
       
    


    return cell
    

indexPathArray: 是可见的行。

vIndexPath:最后一个索引路径可见

加载数据

 func loadData()

       if(isReloadTable)
       let HUD = MBProgressHUD.showAdded(to: self.view, animated: true)
       let manager :AFHTTPSessionManager = AFHTTPSessionManager()
     
       
           var param = NSDictionary()
           param = [
               "category":cat_id,
               "smart_user_id": USERDEF.value(forKey: "user_id") as! String,
               "page":page,
               "phone":phone! as String
               
           ] as [String : Any] as NSDictionary
           print("param1 = \(param)")

           manager.get("lists.php?", parameters: param, progress: nil, success:  (task:URLSessionDataTask, responseObject: Any) in
               
     
                       let adsArray =  dic["results"] as! NSArray;
                       for item in adsArray 
                           let item  = ComplainListItem(dictionary: item as! NSDictionary )
                           self.dataArray.add(item)
                       
                   
                       self.view.addSubview(self.cityTableView)
                       self.cityTableView.reloadData()
                   
                   if(adsArray.count==10)
                       self.cityTableView.reloadData()
                       self.isReloadTable = true
                       self.page+=1
                   else if(adsArray.count<10)
                       self.cityTableView.reloadData()
                       self.isReloadTable = false
               
               
               HUD.hide(animated:true)
               
           )  (operation,error) -> Void in
               print("error = \(error)")
               HUD.hide(animated:true)
           
       
        
   

检查您的 dataArray 计数,即 myadsarray 检查是否等于您的数据限制。然后如果dataArray count等于下一页,如果不等于小于10,则调用所有数据。

【讨论】:

以上是关于UITableView 在像 Facebook 应用程序一样滚动到底部时加载更多的主要内容,如果未能解决你的问题,请参考以下文章

像 Facebook 应用一样的 UITableView

IOS:如何将 UITableView 用作 Facebook 类应用程序的 Newsfeed 视图

Facebook 在 uitableview 中加载相册,但未在下一个收藏视图中打开

UITableView 滚动时隐藏工具栏元素(类似于 Facebook 的应用?)

Swift:UITableView - 滑动删除 - 如何让单元格内容在滑动时不移动

Facebook Graph API GET 请求 - 应包含“字段”参数(Swift,Facebook SDK v4.5.1)