Swift tableView 分页

Posted

技术标签:

【中文标题】Swift tableView 分页【英文标题】:Swift tableView Pagination 【发布时间】:2016-05-23 21:23:57 【问题描述】:

我已经成功地使用 json 解析代码工作了 tableview。但是可能还有 1000 多个项目,所以在滚动底部时需要分页。我不知道如何在下面的代码中执行此操作。对于objective-c 有很多示例,但对于swift 我没有找到工作示例。我在等你的帮助。我想会帮助太多人。谢谢 !

import UIKit

class ViewController: UIViewController, UITableViewDataSource,UITableViewDelegate 

    let kSuccessTitle = "Congratulations"
    let kErrorTitle = "Connection error"
    let kNoticeTitle = "Notice"
    let kWarningTitle = "Warning"
    let kInfoTitle = "Info"
    let kSubtitle = "You've just displayed this awesome Pop Up View"


    @IBOutlet weak var myTableView: UITableView!
    @IBOutlet weak var myActivityIndicator: UIActivityIndicatorView!

    var privateList = [String]()

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

    

    override func viewWillAppear(animated: Bool) 
        super.viewWillAppear(animated)

        loadItems()

    

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


    internal func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    
        return privateList.count
    




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

       let cell:myCell = tableView.dequeueReusableCellWithIdentifier("myCell") as! myCell

        cell.titleLabel.text = privateList[indexPath.row]


        return cell
    


    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) 

        if (editingStyle == UITableViewCellEditingStyle.Delete)

         print(indexPath.row)


            let alert = SCLAlertView()
            alert.addButton("Hayır") 
            alert.addButton("Evet") 

                self.myTableView.beginUpdates()

                 self.privateList.removeAtIndex(indexPath.row)
                tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Left)
                print("Silindi")

                self.myTableView.endUpdates()

                  self.loadItems()

            
            alert.showSuccess(kSuccessTitle, subTitle: kSubtitle)

        


    





    func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool 
        // the cells you would like the actions to appear needs to be editable
        return true
    



    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) 


        if(segue.identifier == "Detail") 

            let destinationView = segue.destinationViewController as! DetailViewController

            if let indexPath = myTableView.indexPathForCell(sender as! UITableViewCell) 

                destinationView.privateLista = privateList[indexPath.row]

            
        
    



    internal func tableView(tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
    
        return 0.0
    


    func loadItems()
    
     loadItemsNow("privateList")

    

    func loadItemsNow(listType:String)
        myActivityIndicator.startAnimating()
        let listUrlString =  "http://bla.com/json2.php?listType=" + listType + "&t=" + NSUUID().UUIDString
        let myUrl = NSURL(string: listUrlString);
        let request = NSMutableURLRequest(URL:myUrl!);
        request.HTTPMethod = "GET";

        let task = NSURLSession.sharedSession().dataTaskWithRequest(request) 
            data, response, error in

            if error != nil 
                print(error!.localizedDescription)
                dispatch_async(dispatch_get_main_queue(),
                    self.myActivityIndicator.stopAnimating()
                )

                return
            


            do 

                let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as? NSArray

                if let parseJSON = json 


                        self.privateList = parseJSON as! [String]

                

             catch 
                print(error)

            

            dispatch_async(dispatch_get_main_queue(),
                self.myActivityIndicator.stopAnimating()
                self.myTableView.reloadData()
            )


        

        task.resume()
    



【问题讨论】:

您是否一次获取数千个对象? @ShehzadAli 不,但在滚动底部时需要刷新新的会很好。还有很多项目会花费很多时间。 【参考方案1】:

另一种方法是:您可以在每次发送请求时设置获取元素的阈值:

假设您是第一次获取 20 个元素。您将保存最后获取的记录 ID 或编号,以获取接下来 20 个元素的列表。

let lastFetchedIndex = 20;

我假设您已经在 myArray.xml 中添加了这些记录。 MyArray 是 tableView 的数据源。现在 myArray 包含 40 个对象。我要列出现在需要在 tableView 中插入的行的 indexPaths 列表。

var indexPathsArray = [NSIndexPath]()


for index in lastFetchedIndex..<myArray.count
    let indexPath = NSIndexPath(forRow: index, inSection: 0)
    indexPathsArray.append(indexPath)


在这里我正在更新我的 tableView。确保您的数据源我的意思是您的 myArray 已经更新。以便它可以正确插入行。

self.tableView.beginUpdates()
tableView!.insertRowsAtIndexPaths(indexPathsArray, withRowAnimation: .Fade)
self.tableView.endUpdates()

【讨论】:

感谢老兄的回答,但我在哪里可以添加代码? 我已经分步说明了。在获得大量新对象后,使用此代码更新您的代码。将来自服务器的项目块添加到用作 tableview 数据源的数组中。然后在您的代码中添加上述代码。上面的代码可以放在一个方法中。 请编辑您的答案并过去我的原始代码并使用您的分页代码进行编辑,稍后我会检查,如果有效,我会批准您的答案..【参考方案2】:

为此,您还需要更改服务器端。

    服务器将接受API url 中的fromIndexbatchSize 作为查询参数。

    let listUrlString =  "http://bla.com/json2.php?listType=" + listType + "&t=" + NSUUID().UUIDString + "&batchSize=" + batchSize + "&fromIndex=" + fromIndex
    

    在服务器响应中,会有一个额外的密钥totalItems。这将用于识别是否收到所有物品。数组或项目 fromIndexbatchSize 的项目数。

在应用端

    首先将使用fromIndex = 0batchSize = 20 调用loadItem()(例如在viewDidLoad()viewWillAppear 中)。在第一次调用 loadItem() 之前从 privateList 数组中删除所有项目

    服务器返回一个包含前 20 个项目的数组和totalItems 服务器中的项目总数。

    追加privateList数组中的20个项目并重新加载tableView

    tableView:cellForRowAtIndexPath 方法中检查单元格是否是最后一个单元格。并检查totalItems(表单服务器)是否大于privateList.count。这意味着服务器中有更多的项目要加载

    if indexPath.row == privateList.count - 1  // last cell
        if totalItems > privateList.count  // more items to fetch
            loadItem() // increment `fromIndex` by 20 before server call
        
    
    

问题: where is refresh ? will be scrolling ?

收到服务器响应后,在数组中添加新项目后刷新。 (第 3 步)

当用户滚动时,每个单元格都会触发tableView:cellForRowAtIndexPath。代码正在检查它是否是最后一个单元格并获取剩余的项目。 (第 4 步)

已添加示例项目:https://github.com/rishi420/TableViewPaging

【讨论】:

刷新在哪里?会滚动吗? 你能写完整代码集成完整代码我将测试 你能写完整代码集成完整代码我将测试 @SwiftDeveloper 答案更新为示例项目链接。下载、运行并查看print 日志。 :) 给我一个示例 url http://bla.com/json2.php?listType=... 我可以点击和测试【参考方案3】:

我在一个项目中需要类似的东西,我的解决方案是:

1 - 创建一个变量 numberOfObjectsInSubArray(初始值 30 或任何你想要的)

2 - 每次点击“显示更多”时,创建一个子数组以从您的 privateList 数组中添加一些对象

    let subArray = privateList?.subarrayWithRange(NSMakeRange(0, numberOfObjectsInSubArray))

然后用它来

internal func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int

    return subArray.count

3- 每当您需要显示更多对象时,请执行以下操作:

func addMoreObjectsOnTableView () 

    numberOfObjectsInSubArray += 30

    if (numberOfObjectsInSubArray < privateList.count) 

        subArray = privateList?.subarrayWithRange(NSMakeRange(0, numberOfObjectsInSubArray))  

     else 

        subArray = privateList?.subarrayWithRange(NSMakeRange(0, privateList.count))  
    

    tableView.reloadData()

希望对你有帮助

【讨论】:

【参考方案4】:

在您的表格视图中添加另一个部分,让该部分只有 1 行,这将是一个包含活动指示器的单元格,以表示正在加载。

internal func numberOfSectionsInTableView(tableView: UITableView) -> Int

    return 2;


internal func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
    
        if section == 0 
            return privateList.count
         else if section == 1     // this is going to be the last section with just 1 cell which will show the loading indicator
            return 1
        
    

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

   if section == 0 
       let cell:myCell = tableView.dequeueReusableCellWithIdentifier("myCell") as! myCell

        cell.titleLabel.text = privateList[indexPath.row]


        return cell
     else if section == 1  
        //create the cell to show loading indicator
        ...

        //here we call loadItems so that there is an indication that something is loading and once loaded we relaod the tableview
        self.loadItems()
    

【讨论】:

其实我以为这个方法行得通,可惜不行,Number of rows抛出了错误,需要在If bock之外添加return语句,CellforRow这个错误抱怨缺少return Cell外If 块,修复了这个问题,但即使是艰难的第 1 节也足够可见并且在桌子的下方,它仍然会触发加载项......【参考方案5】:

在 tableview 中使用scrollviewDelegate 是一种很好且有效的方法 只需在您的viewController 中添加UIScrollViewDelegate 在视图控制器中

//For Pagination
var isDataLoading:Bool=false
var pageNo:Int=0
var limit:Int=20
var offset:Int=0 //pageNo*limit
var didEndReached:Bool=false
viewDidLoad(_)
tableview.delegate=self //To enable scrollviewdelegate

覆盖此委托的两个方法

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) 

        print("scrollViewWillBeginDragging")
        isDataLoading = false
    



    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) 
        print("scrollViewDidEndDecelerating")
    
    //Pagination
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) 

            print("scrollViewDidEndDragging")
            if ((tableView.contentOffset.y + tableView.frame.size.height) >= tableView.contentSize.height)
            
                if !isDataLoading
                    isDataLoading = true
                    self.pageNo=self.pageNo+1
                    self.limit=self.limit+10
                    self.offset=self.limit * self.pageNo
                    loadCallLogData(offset: self.offset, limit: self.limit)

                
            


    

【讨论】:

偏移的目的是什么?,self.limit是我假设的pageSize。【参考方案6】:

这里是集合视图的示例代码:

var page = 0

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
    print("page Num:\(page)")


func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
     if arrImagesData.count-1 == indexPath.row && arrImagesData.count%10 == 0
        getMoreImages(page)
     


func getMoreImages(page:Int) 
   //hit api
   if api_success == true 
       if self.page == 0 
          self.arrImagesData.removeAll()
       
   self.arrImagesData.appendContentsOf(api_data)
   self.collectionImages.reloadData()
   self.page = self.page + 1
   

【讨论】:

【参考方案7】:

SWIFT 3.0 和 4.0

如果您在 API 请求中发送页码,那么这是在您的应用中实现分页的理想方式。

    用初始值为 0 和 bool 声明变量 current Page 以检查是否正在加载任何初始值为 false 的列表
    var currentPage : Int = 0
    var isLoadingList : Bool = false
    这是获取列表示例的函数:
    func getListFromServer(_ pageNumber: Int)
        self.isLoadingList = false
        self.table.reloadData()
    
    这是增加页码并调用 API 函数的函数
   func loadMoreItemsForList()
       currentPage += 1
       getListFromServer(currentPage)
   
   
    这是scrollView滚动时调用的方法
    func scrollViewDidScroll(_ scrollView: UIScrollView) 
        if (((scrollView.contentOffset.y + scrollView.frame.size.height) > scrollView.contentSize.height ) && !isLoadingList)
            self.isLoadingList = true
            self.loadMoreItemsForList()
        
    

附: bool isLoadingList 的作用是防止滚动视图一拖就拉到更多列表到表格视图的底部。

【讨论】:

reloadData() 是否会让用户滚动到顶部? no reloadData() 不会滚动到顶部,使用tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top , animated: true) 滚动到顶部 我已经完美地实现了您的功能,但是当我滚动得更快或页面计数到 4 或 5 不包含数据时,它给了我index out of bound 错误。如何持续显示数据?【参考方案8】:

现在在 ios10 中添加了一个新协议,这变得更容易了:UITableViewDataSourcePrefetching

https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching

【讨论】:

这应该是公认的答案。这是一个关于如何使用这个协议的很好的教程:raywenderlich.com/5786-uitableview-infinite-scrolling-tutorial.【参考方案9】:

我尝试了一种使用 willDisplayCell 的方法。但它会在滚动过程中产生不必要的停止,从而使用户体验不佳。 我认为更好的方法是在 scrollViewDidEndDecelerating 委托方法中做到这一点。它在滚动完成时调用,然后才出现新数据。用户看到有新内容,如果他愿意,可以再次滚动。我已经接受了here 的答案,但我使用的是 scrollViewDidEndDecelerating 而不是 scrollViewDidEndDragging。就我而言,它看起来更好。这是我项目中的一些代码。

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) 
    guard scrollView == tableView,
        (scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height,
        !viewModel.isLastPeriodicsPage else  return 

    viewModel.paginatePeriodics(tableView.getLastIndexPath())

【讨论】:

你能解释一下这个例子中 viewModel.paginatePeriodics() 是什么吗? 这个方法调用我们的服务器 API 来获取新部分的项目,从给定的索引开始,没什么特别的 此方法存在问题。这取决于用户滚动页面。当屏幕上有空间显示更多项目时,此方法会失败,但它会等待用户滚动触发 API 调用以进行下一页加载。【参考方案10】:

制作了一个通用的分页框架:?

https://github.com/eonist/PaginationTable

let table = Table(rowData: [], frame: .zero, style: .plain)
  view = table
  table.isFetching = true
  Table.fetchData(range: table.paginationRange)  rowItem in
     DispatchQueue.main.async  [weak table] in
        table?.rowData += rowItem
        table?.reloadData()
        table?.paginationIndex += Table.paginationAmount // set the new pagination index
        table?.isFetching = false
     
  

【讨论】:

【参考方案11】:

通过UITableViewDelegate,你可以调用函数

   func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
    let lastItem = self.mes.count - 1
    if indexPath.row == lastItem 
        print("IndexRow\(indexPath.row)")
        if currentPage < totalPage 
            currentPage += 1
           //Get data from Server
        
    

【讨论】:

【参考方案12】:
//It works fine 
func getPageCount(TotalCount : Int) -> Int
    var num = TotalCount
    let reminder = num % 50
    print(reminder)
    if reminder != 0
        num = TotalCount/50
        num = num + 1

    else
        num = TotalCount/50
    
    return num


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 
    let TotalPage =  self.getPageCount(TotalCount: Int(Datacount)!)
    let lastItem = self.mainArr.count - 1
    if indexPath.row == lastItem 
        print("IndexRow\(indexPath.row)")
        if self.page < TotalPage-1 
            self.view_Loader.isHidden = false
            self.view_LoaderHeight.constant = 50
        self.page += 1
        self.YourAPI()
        
    
`

【讨论】:

【参考方案13】:

API 处理程序是用于网络调用的 api 处理程序,只进行 POST 和 GET 调用。 getNotifications 基本上只是一个带有参数(偏移量和 pageSize)的后调用,并且作为响应有列表。 主要逻辑是根据 willDisplay collectionView 委托中的单元格更改偏移量。如果您有任何问题,请发表评论,很乐意提供帮助。

var isFetching: Bool = false
var offset = 0
var totalListOnServerCount = 20 // it must be returned from server
var pageSize = 10 // get 10 objects for instance
// MARK: - API Handler
private func fetchNotifications()
    // return from function if already fetching list
    guard !isFetching else return
        if offset == 0
            // empty list for first call i.e offset = 0
            self.anyList.removeAll()
            self.collectionView.reloadData()
        
        isFetching = true
        // API call to fetch notifications with given offset or page number depends on server logic just simple POST Call
        APIHandler.shared.getNotifications(offset: offset) [weak self] (response, error) in
            if let response = response 
                self?.isFetching = false
                if self?.offset == 0
                    // fetch response from server for first fetch
                    self?.notificationsResponse = response
                    if self?.refreshControl.isRefreshing ?? false 
                        self?.refreshControl.endRefreshing()
                    
                else
                    // append if already exist ( pagination )
                    self?.notificationsResponse?.notifications.append(contentsOf: response.notifications)
                
                self?.collectionView.reloadData()

            

        



// MARK: - Collection View Delegate
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) 

    guard let anyList = responseFromServer else  return 
    // check if scroll reach last index available and keep fetching till our model list has all entries from server
    if indexPath.item == anyList.count - 1 && anyList.count  < totalListOnServerCount

        offset += pageSize
        fetchNotifications()

    

【讨论】:

【参考方案14】:

Swift 5(全面全面的分页解决方案)

用户界面代码: https://github.com/eonist/PaginationTable

数据模型代码: https://github.com/eonist/PaginationService

核心组件:

rowData:此数组将在每个滚动结束事件上增长,直到它从后端 API 加载所有项目 paginationAmount:每个分页周期获取的数量 paginationIndex:当前单元格数量(随着您加载更多数据而增长 isFetching:一个布尔值,让代码知道数据是否已经加载,以避免重复获取等 fetchData:模拟从 remote-api 获取数据 陷阱:

示例代码不依赖于后端。它只是使用文件中的数据进行测试,并通过休眠几秒钟来模拟网络调用 该示例使用了一些依赖项以加快此示例的创建速度。但它的基本内容,如 AFNetwork、Json 解析、Autolllayout。所有这些都可以轻松替换 要求:

可以提供项目计数的后端 API 可以返回范围内项目的后端 API(startIndex、endIndex)

【讨论】:

以上是关于Swift tableView 分页的主要内容,如果未能解决你的问题,请参考以下文章

调整 TableView 大小时,Swift 3.0 表格单元格未填充

Swift:tableView 行的高度,其 tableView 单元格具有动态行数的嵌套 tableView

如何使用 MovieDB API Swift 为 TableView 进行分页?

Swift中带有标题的TableView的自定义分页大小(一次滚动一个单元格)

Swift:在滚动时更改表格视图高度

使用 Firebase Firestore 进行分页 - swift 4