I implement pull to refresh in collection View and the problem I'm facing is my app will crash with out of index message. Below is my cellForItem method
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CauNguyenCell
cell.postArray = postData[indexPath.item]
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return postData.count
}
I know the problem is because I use removeAll method to clear all data from my postData but I have to that so my data array will have completely new data.
Below is my refresh function:
func handleRefresh(_ refreshControl: UIRefreshControl) {
refreshControl.beginRefreshing()
postData.removeAll()
fetchDataAgain()
self.collectionView.reloadData()
refreshControl.endRefreshing()
}
error message: Thread 1: Fatal error: Index out of range
I just want to ask if anyone has any suggestions to solve the problem. Thanks!
I have implemented same thing in my project. First I have created refreshControl instance globally, then setup in initSetup() method call from viewDidLoad() in my view controller.
var refreshControl : UIRefreshControl?
var arrWeddingInvitations = [MyModelClass]()
func initialSetup() {
self.refreshControl = UIRefreshControl()
self.refreshControl?.tintColor = .yellow
self.refreshControl?.addTarget(self, action: #selector(getWeddingInvitations), for: .valueChanged)
self.cvWeddingInvitation.addSubview(refreshControl!)
}
This is call getWeddingInvitations() method which is fetching data from server.
// This code will hide refresh controller
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
}
// This code in my API request completion block and will check responded array data, If it is not nil then assigned to my global array in view controller which are used to display data and reload collection view.
if arrInvitations != nil, arrInvitations.count > 0 {
self.lblEmptyData.isHidden = true
self.cvWeddingInvitation.isHidden = false
self.arrWeddingInvitations = arrInvitations
self.cvWeddingInvitation.reloadData()
} else {
self.lblEmptyData.isHidden = false
self.cvWeddingInvitation.isHidden = true
}
This is working code in my current project. I hope this will help you.
See following video:
Pull to refresh test video
Please check your UICollectionViewDataSource implementation for these methods:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
func numberOfSections(in: UICollectionView)
In the first one you should return the current number of items postData.count, the second one in your case should return 1.
Edit you code like this:
func fetchDataAgain(completion: ((Bool) -> ())) {
// your code
if postData != nil, postData.count != 0 {
completion(true)
}
}
func handleRefresh(_ refreshControl: UIRefreshControl) {
refreshControl.beginRefreshing()
postData.removeAll()
fetchDataAgain { (complete) in
if complete {
self.collectionView.reloadData()
refreshControl.endRefreshing()
}
}
}
Method fetchDataAgain will check array, and if it != nil and count != 0 in handler complete reloadData.
Code do all step by step, and when you are reload data in collection, your array can be empty, or nil. As a rule better to use handlers
I have one api call, in that where i will fetch all the names and i am appending to one var to display in my collection view label.but values are not appending to my var.
here code :
var mobkam = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.getallLoans()
}
func getallLoans(){
Manager.sharedInstance.getallLoans { (data, err) in
if let _ = err{
}else{
if let dataa = data as? String{
if let dataFromString = dataa.data(using: String.Encoding.utf8, allowLossyConversion: false) {
let json = JSON(data: dataFromString)
print(json) // correctly display all names like ["1","2", etc]
self.mobileOprator.removeAll()
for (_, val) in json {
print(val.rawString()) // displaying the correct each items names
self.mobkam.append(val.rawString()!)
}
}
}
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.mobkam.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! NameCollectionViewCell
cell.NameLabel.text = self.mobkam[indexPath.item]
return cell
}
What i m doing wrong?. Not able to solve.Am i missed any.Please help me out.
Thanks !
You just need to call collectionView.reloadData() after you have loaded all of the values in getallLoans.
When you make the API call, this is an asynchronous task that will take a little time to complete. Your collection view will have already loaded it's data source so you need to inform it that the data has changed. It will then call the CollectionViewDataSource delegate methods again and refresh the view based on the updated data.
for (_, val) in json {
print(val.rawString()) // displaying the correct each items names
self.mobkam.append(val.rawString()!)
}
collectionView.reloadData()
Problem: TableView is very slow when scrolling. Looks like my code is not efficient at all.
So I have a UICollectionView embedded inside a tableViewCell like so (I used this tutorial to accomplish it.)
I am using Firebase to populate data into the UICollectionViewCells. I have 3 class folders:
TableViewCtrl: Responsible for downloading section titles and then passing some logic to tableViewCell. Here is partial code of the main TableViewCtrl:
// 1. DOWNLOAD SECTION TITLES AND THEN CALL RELOADTABLE
// 2. TableView:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return featuredCollection.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "browseCell", for: indexPath) as! BrowseCell
cell.configureCell(of: featuredCollection[indexPath.row].typeOfItem, title: featuredCollection[indexPath.row].categoryTitle, bookReference: featuredCollection[indexPath.row].bookReference, spotlightTests: featuredCollection[indexPath.row].spotlightTests, bookUniqueIDsToDownload: featuredCollection[indexPath.row].bookUniqueIDsToDownload)
return cell
}
TableViewCell:
func configureCell(of type: FeaturedItem, title: String, bookReference: BookReferenceTest?, spotlightTests: [BookReferenceTest]?, bookUniqueIDsToDownload: [String]?) {
setCollectionViewDataSourceDelegate(delegate: self, dataSource: self)
// DOWNLOAD THE BOOK ITEMS (eg. IMAGES, TITLES, ETC) then call self.collectionView.reloadData()
}
internal func setCollectionViewDataSourceDelegate <D: UICollectionViewDelegate, S: UICollectionViewDataSource>(delegate: D, dataSource: S) {
collectionView.delegate = delegate
collectionView.dataSource = dataSource
let collectionViewFlowLayout = UICollectionViewFlowLayout()
let myCollectionView = UICollectionView(frame: self.collectionView.bounds, collectionViewLayout: collectionViewFlowLayout)
myCollectionView.delegate = self
myCollectionView.dataSource = self
collectionView.reloadData()
}
// Snipit of code that's responsible for downloading book assets:
func downloadBrowsingBooks(bookUniqueKeys: [String]) {
let databaseReference = FIRDatabase.database().reference()
databaseReference.child("Kutub/Books/").observeSingleEvent(of: .value, with: {
(snapshot) in
var books = [BrowsingBook]()
for book in (snapshot.children.allObjects as! [FIRDataSnapshot]) {
if bookUniqueKeys.contains((book.key)) {
let browsingBookValues = book.value as! [String : AnyObject]
let browsingBook = self.createBrowsingBookObject(data: browsingBookValues, uniqueKey: book.key)
books.append(browsingBook)
}
}
self.storedBooks = books
self.collectionView.reloadData()
})
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "booksCollectionCell", for: indexPath) as! BooksCollectionCell
let bookTitle = storedBooks[indexPath.item].title
let authors = storedBooks[indexPath.item].authors
cell.configureCell(title: bookTitle, authorNames: authors)
return cell
}
UICollectionViewCell:
func configureCell(title: String, authorNames: [String]? = nil, imageCover: UIImage? = nil) {
var authorName = ""
if let authors = authorNames {
authorName = authors[0]
for index in 1..<authors.count {
authorName += ", \(authors[index])"
}
}
// ....
}
From my understanding, here's step-by-step of what's happening:
Section titles are downloaded
TableView.reload() configures the tableViewCells
Inside tableViewCells, firebase downloads images and other book assets (eg. titles, authors, publishers names in text from Firebase database) and calls on collectionView
CollectionView configures it's cells.
Again, my main problems is that scrolling is very slow and laggy with the way that I'm doing this. When I tried different methods (eg. downloading the data and passing it on to tableviewCell) it works but when I add items to Firebase database only section titles show up and not the content inside the collectionViewCells.
I learned the UICollectionView inside UITableViewCell from this tutorial.
It works perfectly only when all of the collection views have the same numberOfItemsInSection, so it can scroll and won't cause Index out of range error, but if the collection view cells numberOfItemsInSection are different, when I scroll the collection view it crashes due to Index out of range.
I found the reason that when I scroll the tableview, the collection view cell index path updated according to the bottom one, but the collection view I scrolled is top one so it does't remember the numberOfItemsInSection, so it will crash due to Index out of range.
ps: it is scrollable, but only when that table cell at the bottom, because its numberOfItemsInSection is correspond to that cell and won't cause Index out of range.
This is the numberOfItemsInSection of My CollectionView, i actually have two collection view but i think this is not the problem
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var numberOfItemsInSection: Int = 0
if collectionView == self.collectionview_categorymenu {
let categories = self.json_menucategory["menu_categories"].arrayValue
numberOfItemsInSection = categories.count
} else {
let menuitems = self.json_menucategory["menu_items"].arrayValue
let item_data = menuitems[self.itemimages_index].dictionaryValue
let menu_item_images = item_data["menu_item_images"]?.arrayValue
numberOfItemsInSection = (menu_item_images?.count)!
print(numberOfItemsInSection)
}
return numberOfItemsInSection
}
This is the cellForItemAt indexPath of my CollectionView:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == self.collectionview_categorymenu {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CategoryMenuCell", for: indexPath) as! CategoryMenuCell
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ItemImagesCell", for: indexPath) as! ItemImagesCell
let menuitems = self.json_menucategory["menu_items"].arrayValue
let item_data = menuitems[self.itemimages_index].dictionaryValue
let menu_item_images = item_data["menu_item_images"]?.arrayValue
print(menu_item_images!)
let itemimage_data = menu_item_images?[indexPath.item].dictionaryValue
print("===\(indexPath.item)===")
let itemimage_path = itemimage_data?["image"]?.stringValue
let itemimage_detail = itemimage_data?["detail"]?.stringValue
if let itemimage = cell.imageview_itemimage {
let image_url = itemimage_path?.decodeUrl()
URLSession.shared.dataTask(with: NSURL(string: image_url!) as! URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print("error")
return
}
DispatchQueue.main.async(execute: { () -> Void
in
itemimage.image = UIImage(data: data!)!
})
}).resume()
}
if let description = cell.textview_description {
description.text = itemimage_detail
}
if let view = cell.view_description {
view.isHidden = true
}
return cell
}
}
The problem is probaby that you are returning the same number every time for numberOfItemsInSection what you want to do is return the number of items in each array. From the link you sent they do this:
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return model[collectionView.tag].count
}
The other possibility is that you are returning the wrong array when cellForItemAtIndexPath please check that you are using the tag attribute correctly
So they get the count of each array within the model. Without seeing any of your code its very difficult to actually see where your error is coming from.
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()
}
}
For that you need to have server side change also.
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
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
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
Server returns an array of first 20 items and totalItems total number of items in the server.
Append the 20 items in privateList array and reload tableView
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
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.
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
This is the function that gets the list example:
func getListFromServer(_ pageNumber: Int){
self.isLoadingList = false
self.table.reloadData()
}
This is the function that increments page number and calls the API function
func loadMoreItemsForList(){
currentPage += 1
getListFromServer(currentPage)
}
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.
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)
}
}
}
This is now a little bit easier with the addition of a new protocol in iOS10: UITableViewDataSourcePrefetching
https://developer.apple.com/documentation/uikit/uitableviewdatasourceprefetching
//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()
}
}
}`
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
}
}
}
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
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())
}
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()
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()
}
}
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
}
}
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()
}
}
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
}
}
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)