77

I have success working tableview with json parsing code. But may have 1000 more item so I need pagination when scrolling bottom side. I don't know how can I do this for my code shown below. For objective-C, there are a lot of examples but for Swift I didn't find a working example.

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()
    }
}
starball
  • 20,030
  • 7
  • 43
  • 238
SwiftDeveloper
  • 7,244
  • 14
  • 56
  • 85

14 Answers14

89

For that you need to have server side change also.

  1. Server will accept fromIndex and batchSize in the API url as query param.

    let listUrlString =  "http://bla.com/json2.php?listType=" + listType + "&t=" + NSUUID().UUIDString + "&batchSize=" + batchSize + "&fromIndex=" + fromIndex
    
  2. In the server response, there will be an extra key totalItems. This will be used to identify all items are received or not. An array or items fromIndex to batchSize number of items.

In the app side

  1. First loadItem() will be called with fromIndex = 0 and batchSize = 20 (for example in viewDidLoad() or viewWillAppear). removeAll items from privateList array before calling loadItem() for the first time

  2. Server returns an array of first 20 items and totalItems total number of items in the server.

  3. Append the 20 items in privateList array and reload tableView

  4. In tableView:cellForRowAtIndexPath method check if the cell is the last cell. And check if totalItems (form server) is greater than privateList.count. That means there are more items in the server to load

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

Question: where is refresh ? will be scrolling ?

Refresh after appending new items in the array when server response received. (step 3)

Scrolling will trigger tableView:cellForRowAtIndexPath for every cell when user scrolls. Code is checking if it is the last cell and fetch remaining items. (step 4)

Sample project added:
https://github.com/rishi420/TableViewPaging

Warif Akhand Rishi
  • 23,920
  • 8
  • 80
  • 107
  • where is refresh ? will be scrolling ? – SwiftDeveloper Feb 18 '16 at 11:55
  • can you write full code integrated full code i will test – SwiftDeveloper Feb 18 '16 at 12:33
  • can you write full code integrated full code i will test – SwiftDeveloper Feb 18 '16 at 16:44
  • 2
    @SwiftDeveloper Answer updated with Sample project link. Download, run and see the `print` log. – Warif Akhand Rishi Feb 18 '16 at 19:23
  • pagining but with simple local data , i want to use it with my json data. ! like your codes with simple local data(1,2,3,4,5) have many example . i want to use pagination with json data like my codes. just add pagination with my http://bla.com/json2.php?listType=" + listType + "&showitem=20 like this. i will add mysql into order by limit showitem,20 like .. – SwiftDeveloper Feb 18 '16 at 19:35
  • do it with simple edit your github project i will approve your answer quickly – SwiftDeveloper Feb 18 '16 at 19:36
  • First part of my answer is about change in the server. your `bla.com....` server needs to have pagination facility with `batchSize` and `fromIndex` – Warif Akhand Rishi Feb 18 '16 at 19:45
  • just integrate that codes into my own codes i will write server side codes dont care later i will approve your answer – SwiftDeveloper Feb 18 '16 at 21:08
  • I will approve today 1 answer so im triying your codes now i added all into my codes but i didnt do correctly func loadItemsNow() inside your let endIndex = min(totalItems, fromIndex + batchSize) for i in fromIndex ..< endIndex { privateList.append(String(i)) } print("Loading items form \(fromIndex) to \(endIndex - 1)") fromIndex = endIndex aTableView.reloadData() codes. where can i add them into my func loadItemsNow() { im waiting your reply – SwiftDeveloper Feb 23 '16 at 09:09
  • please edit your answer and past my orginal codes and edit them with your pagination codes later i will check and i will approve your answer .. – SwiftDeveloper Feb 23 '16 at 09:13
  • @SwiftDeveloper remove the `for` loop and call `loadItemsNow()` instead. Inside the `loadItemsNow()` change `self.privateList = parseJSON as! [String]` to `privateList.appendContentsOf(parseJSON as! [String])` – Warif Akhand Rishi Feb 23 '16 at 10:03
  • dude why you cannot write that codes here ? i will test and i will approve write integrated codes here i did gives non member appendContentsOf error – SwiftDeveloper Feb 23 '16 at 10:11
  • 2
    :) give me an example url `http://bla.com/json2.php?listType=...` where i can hit and test – Warif Akhand Rishi Feb 23 '16 at 10:14
28

SWIFT 3.0 and 4.0

If you're sending the page number in the API request then this is the ideal way for implementing pagination in your app.

  1. declare the variable current Page with initial Value 0 and a bool to check if any list is being loaded with initial value false
    var currentPage : Int = 0
    var isLoadingList : Bool = false
  1. This is the function that gets the list example:
    func getListFromServer(_ pageNumber: Int){
        self.isLoadingList = false
        self.table.reloadData()
    }
  1. This is the function that increments page number and calls the API function
   func loadMoreItemsForList(){
       currentPage += 1
       getListFromServer(currentPage)
   }
   
  1. this is the method that will be called when the scrollView scrolls
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if (((scrollView.contentOffset.y + scrollView.frame.size.height) > scrollView.contentSize.height ) && !isLoadingList){
            self.isLoadingList = true
            self.loadMoreItemsForList()
        }
    }

P.S. the bool isLoadingList role is to prevent the scroll view from getting more lists in one drag to the bottom of the table view.

Rashid Latif
  • 2,809
  • 22
  • 26
MhmdRizk
  • 1,591
  • 2
  • 18
  • 34
  • Does `reloadData()` will make the user scroll to top? – Amber K Nov 14 '18 at 06:34
  • 2
    no reloadData() wont scroll to top, use `tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top , animated: true)` to scroll to top – MhmdRizk Nov 14 '18 at 07:26
  • 1
    I have implemented your function perfectly working, but when I scroll more fast or page count to 4 or 5 which doesnt contain data it gives me `index out of bound ` error. how to keep displaying data? –  Feb 06 '20 at 13:58
22

The good and efficient way to do it is by using scrollviewDelegate in tableview Just add UIScrollViewDelegate in your viewController In view controller

//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
}

Override two methods from this delegate

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)

                }
            }


    }
Mahesh Giri
  • 1,810
  • 19
  • 27
19

This is now a little bit easier with the addition of a new protocol in iOS10: UITableViewDataSourcePrefetching

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

elasticrat
  • 7,060
  • 5
  • 36
  • 36
  • 2
    This should be the accepted answer. Here is a nice tutorial on how to use this protocol: https://www.raywenderlich.com/5786-uitableview-infinite-scrolling-tutorial. – iurii Apr 17 '19 at 16:23
6
//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()
        }
    }
}`
Tamir Abutbul
  • 7,301
  • 7
  • 25
  • 53
5

By using UITableViewDelegate, u can call the function

   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
        }
    }
}
3

I needed something similar on a project and my solution was:

1 - create a variable numberOfObjectsInSubArray (initial value 30 or whatever you want)

2 - create a subarray to add a number of objects from your privateList array every time i tap "show more"

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

And use it on

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

3- Whenever you need to show more objects, do:

func addMoreObjectsOnTableView () {

    numberOfObjectsInSubArray += 30

    if (numberOfObjectsInSubArray < privateList.count) {

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

    } else {

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

    tableView.reloadData()
}

I hope it helps

scollaco
  • 947
  • 8
  • 13
3

I've tried an approach with willDisplayCell. But it produces unwanted stops during scrolling which makes the user experience not good. I think a better way is to do it in scrollViewDidEndDecelerating delegate method. It calls when the scroll finishes and only then new data comes. User sees that there is new content and scroll again if he wants. I've taken the answer here but instead of scrollViewDidEndDragging I use scrollViewDidEndDecelerating. It looks just better in my case. Here is some code from my project.

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())
}
  • Could you please explain what does viewModel.paginatePeriodics() in this example? – Abrcd18 Mar 17 '21 at 17:21
  • 1
    This method calls our server API to get the new portion of items, starting from the given index, nothing fancy – Andriy Pohorilko Apr 08 '21 at 11:07
  • There is an issue with this method. It depends on user scrolling the page. This approach fails when there is space on the screen to show more items, but it would wait for user's scroll to trigger the API call for next page load. – Abdul Mateen Nov 16 '21 at 10:09
2

Another way of doing this is: You may set a threshold for getting elements while sending request each time:

Lets say you you are fetching 20 elements first time. You will be saving last fetched record id or number for getting list of next 20 elements.

let lastFetchedIndex = 20;

I am assuming that you have already added these records in your myArray. MyArray is the dataSource of tableView. Now myArray is containing 40 objects. I am going to make a list of indexPaths of rows that needs to be inserted in tableView now.

var indexPathsArray = [NSIndexPath]()


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

}

Here I am updating my tableView. Make sure your dataSource i mean your myArray has already been updated. So that it may insert rows properly.

self.tableView.beginUpdates()
tableView!.insertRowsAtIndexPaths(indexPathsArray, withRowAnimation: .Fade)
self.tableView.endUpdates()
Shehzad Ali
  • 1,846
  • 10
  • 16
  • thanks for answer dude but where can i add that codes ? – SwiftDeveloper Feb 12 '16 at 19:20
  • 1
    I have explained it in steps. After getting chunk of new objects update your code with this code. Add the chunk of items coming from server to your array which is being used as datasource of tableview. Then add the above mentioned code in your code. Above code can be placed in one method. – Shehzad Ali Feb 12 '16 at 19:45
  • please edit your answer and past my orginal codes and edit them with your pagination codes later i will check and if it works i will approve your answer .. – SwiftDeveloper Feb 23 '16 at 09:13
2

Add another section to your tableview, let this section have only 1 row which will be a cell containing an activity indicator, to denote loading.

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()
    }
}
Sumeet
  • 1,055
  • 8
  • 18
  • I actually thought this method will work, but unfortunately not, Number of rows threw an error, need to add a return statement outside the If bock, CellforRow thew an error moaning about a lack of return Cell outside the If block, fixed that but only enough even tough Section 1 was to visible and far down the table, it still fired the load items ... – captain_haddock May 06 '21 at 22:14
2

here is a sample code for collection view :

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
   }
}
SwiftyIso
  • 77
  • 2
  • 13
1

API handler is api handler for network call that just do POST and GET calls. getNotifications is basically just a post call with params( offset and pageSize ) and in response there is list. Main logic is changing offset depending on cell in willDisplay collectionView delegate. Comment if you having any question , happy to help.

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

    }
}
Asad Jamil
  • 198
  • 9
0

Made a General purpouse pagination framework:

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
     }
  }
Sentry.co
  • 5,355
  • 43
  • 38
0

Swift 5 (Full comprehensive pagination solution)

The UI code: https://github.com/eonist/PaginationTable

The Data Model code: https://github.com/eonist/PaginationService

Core components:

  • rowData: This array will grow on each scroll-ended-event until it has loaded all items from backend-API
  • paginationAmount: The amount to fetch on each pagination cycle
  • paginationIndex: The current amount of cells (this grows as you load more data
  • isFetching: A boolean that lets the code know if data is already loading or not, to avoid double fetching etc fetchData: Simulates getting data from remote-api Gotchas:

The example code is not reliant on a backend. It simply tests with data from a file and simulates network calls by sleeping for some seconds The example uses some dependencies in order to speed up the creation of this example. But its basic stuff like AFNetwork, Json parsing, Autollayout. All of which could easily be substituted Requirements:

Backend-API that can provide the count of items Backend-API that can return items for a range (startIndex, endIndex)

Sentry.co
  • 5,355
  • 43
  • 38