iOS: Invalid update: invalid number of rows in section 0 - ios

I create a table view that every time I scroll to the end of table it will show shimmer in table section 1 for waiting the api load a new data. After api finish load data, new data will append to the array that use for contain all data that be show to table view on section 0, then reload the table view to update the section 1 numberOfRowsInSection to 0 to hide the shimmer and update the section 0 numberOfRowsInSection
So this is the example of my code
fileprivate var data = [dataArray]() {
didSet {
guard !data.isEmpty else {
return
}
tableView.reloadData()
tableView.layoutIfNeeded()
view.layoutIfNeeded()
}
}
fileprivate var isLoadingMore: Bool = false {
didSet {
tableView.reloadSections(IndexSet(integer: 1), with: .automatic)
tableView.layoutIfNeeded()
tableViewHeightConstraint.constant = tableView.contentSize.height + 60.0
view.layoutIfNeeded()
}
}
fileprivate func loadData() {
if let size = self.paging.totalSize,
data.count >= size {
return
}
let limit = paging.limit
let offset = paging.offset
guard !isLoadingMore else { return }
isLoadingMore = true
controller.requestContent(
completion: { [weak self] (success, offset, size, data) in
DispatchQueue.main.async {
self?.isLoadingMore = false
guard let list = data,
let data = list as? [dataArray],
let size = size else {
return
}
if success {
if self?.paging.currentPage == 0 {
self?.data = data
if self?.scrollView.frame.size.height >= self?.scrollView.contentSize.height {
self?.paging.totalSize = size
self?.paging.currentPage += 1
self?.loadData()
return
}
} else {
self?.data.append(contentsOf: songs)
}
self?.paging.totalSize = size
self?.paging.currentPage += 1
} else {
self?.alert("failed")
}
}
})
}
fileprivate func loadDataFromCoreData() {
isLoadingMore = false
let context = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext
let dataFromCoreData = Datas.fetchData(context: context).filter({$0.isSaved})
songs = dataFromCoreData.map({ dataArray(song: $0) })
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return data.count
case 1:
return isLoadingMore ? 1 : 0
default:
return 0
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height){
loadData()
}
}
func setupForCeckingAvailableData() {
utility.checkInternetAccess = { [weak self] result in
if result {
self?.paging = Paging()
self?.loadData()
} else {
self?.loadDataFromCoreData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupForCeckingAvailableData()
}
so some time when I first time load or try fetch new data by scrolling to the end of table I got a crash with the message is
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
So what the cause of it? Why got that crash even though I already use reloadData() every time I append a new data to variable data. Should I change reloadData() to
tableView.beginUpdates()
tableView.insertRows(at: indexPaths, with: .automatic)
tableView.endUpdates()
? if that so, is that the crash cause insertRows only refresh specific row and section so no take a loot of time to refresh table with a loot of datas??
That's all of my question, Thank you

I just want to point out something else.
Generally speaking, Singletons are Evil and I avoid them like the plague.
This is a real big code smell and would advise you to stop using this type of code:
TableView.beginUpdates()
TableView.insertRows(at: indexPaths, with: .automatic)
TableView.endUpdates()
See What is so bad about singletons? there are many more blog posts about it.

after I do some research I found a clue.
I add tableView.reloadData() before tableView.reloadSections(IndexSet(integer: 1), with: .automatic) because when I see the crashlytic crash happend at tableView.reloadSections(IndexSet(integer: 1), with: .automatic)

Related

Swift expandable table view header for sections problem [duplicate]

This question already has answers here:
How to deal with dynamic Sections and Rows in iOS UITableView
(3 answers)
Closed 3 years ago.
I have question about expandable table view by header. I made it for section zero and its working correctly but when I try to make it same thing for section one too it gives error which is
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
If I didn't trigger second header function, first one still working. When I clicked second header ( which is section one) it gives that error.
Here my header view code:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let button = UIButton(type: .system)
button.setTitle("Kapat", for: .normal)
button.setTitleColor(.black, for: .normal)
button.backgroundColor = .lightGray
button.titleLabel!.font = UIFont.boldSystemFont(ofSize: 14)
button.addTarget(self, action: #selector(handleExpandCloseForAlim(sender:)), for: .touchUpInside)
return button
}
if section == 1 {
let button2 = UIButton(type: .system)
button2.setTitle("Kapat", for: .normal)
button2.setTitleColor(.black, for: .normal)
button2.backgroundColor = .lightGray
button2.titleLabel!.font = UIFont.boldSystemFont(ofSize: 14)
button2.addTarget(self, action: #selector(handleExpandCloseForTeslim(sender:)), for: .touchUpInside)
return button2
}
else{
return nil
}
}
Here my action functions:
#objc func handleExpandCloseForAlim(sender: UIButton) {
var indexPaths = [IndexPath]()
for row in self.userAdressDict.indices{
let indexPath = IndexPath(row: row, section: 0)
indexPaths.append(indexPath)
}
let indexPath = IndexPath(row: (sender.tag), section: 0)
let adresso = self.userAdressDict[indexPath.row]
let isExp = adresso.isExpanded
adresso.isExpanded = !isExp!
if isExp! {
tableView.deleteRows(at: indexPaths, with: .fade)
}else {
tableView.insertRows(at: indexPaths, with: .fade)
}
print(adresso.isExpanded!, adresso.userMahalle!)
}
#objc func handleExpandCloseForTeslim(sender: UIButton) {
var indexPathsForTeslim = [IndexPath]()
for row in self.userAdressDictForTeslim.indices{
let indexPath = IndexPath(row: row, section: 1)
indexPathsForTeslim.append(indexPath)
}
let indexPath = IndexPath(row: (sender.tag), section: 1)
let adresso = self.userAdressDictForTeslim[indexPath.row]
let isExp = adresso.isExpanded
adresso.isExpanded = !isExp!
if isExp! {
tableView.deleteRows(at: indexPathsForTeslim, with: .fade)
}else {
tableView.insertRows(at: indexPathsForTeslim, with: .fade)
}
print(adresso.isExpanded!, adresso.userMahalle!)
}
And here my numberOfRowsInSection part:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
guard let element = self.userAdressDict.first as? adresStruct else { return 0 }
if element.isExpanded {
return self.userAdressDict.count
}
else {
return 0
}
}
if section == 1 {
guard let teslim = self.userAdressDictForTeslim.last as? adresStruct else { return 0 }
if teslim.isExpanded {
return self.userAdressDictForTeslim.count
}
else {
return 0
}
}
return 1
}
I can't find any solution. I assume sender thing working wrongly but it may be another problem. I hope I asked clearly.Please help me.
I am not deep dive in your code but I copy-paste into an Xcode project to look into. So there is basic way to solve your problem, I hope it helps to you.
I assume your class is like that.
class Adres{
var title: String = ""
var isExpanded: Bool = true
init(title: String, isExpanded: Bool){
self.title = title
self.isExpanded = isExpanded
}
}
And in there mainly 2 different variable for adresDict & adresDictForTeslim.
So I keep an lock array to doing logic stuff of expandable.
var keys: [Int] = [1,1]
<1> Show
<0> Hide
Now, the arrays elements are showing into UITableView, because its expanded.
There's mock data.
var userAdressDict = [Adres(title: "First Adres", isExpanded: true) ,Adres(title: "Second Adres", isExpanded: true)]
var userAdressDictForTeslim = [Adres(title: "First Teslim", isExpanded: true),Adres(title: "Second Teslim", isExpanded: true)]
viewForHeaderInSection is same as well.
And for numberOfRowsInSection check array for key that if its expanded or not.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if self.keys[0] == 1 {
return userAdressDict.count
}else{
return 0
}
}else{
if self.keys[1] == 1 {
return userAdressDictForTeslim.count
}else{
return 0
}
}
}
Then check these keys in handler functions.
#objc func handleExpandCloseForAlim(sender: UIButton) {
if keys[0] == 0 {
keys[0] = 1
}else{
keys[0] = 0
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
If first array elements in table view is expanded close it, or not expand it.
Then in another function.
#objc func handleExpandCloseForTeslim(sender: UIButton) {
if keys[1] == 0 {
keys[1] = 1
}else{
keys[1] = 0
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
So far so good. It's working on my simulator.
Then there's more efficient way to handle keys of expanded.
Make your UIButton's of the sectionHeader as public variable in your ViewController and control it if it's title is "Kapat" and clicked then it must be "Aç" or if its "Aç" and touched it must be "Kapat".

Delete item from CollectionView with sections iOS Swift

I have a CollectionView with sections (2 sections). When I delete a cell from section 1 its deleting very good. But when I delete a cell from section 0 my app is crashing with error like this:
invalid number of items in section 0. The number of items contained in an existing section after the update (5) must be equal to the number of items contained in that section before the update (5), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
Before delete item from collectionView, I delete it item from my data source in performBatchUpdates:
extension MainCollectionViewController: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView?.performBatchUpdates({ [weak self] in
guard let self = self else { return }
items = controller.fetchedObjects as! [Item]
items2 = items.chunked(into: 5)
self.collectionView?.deleteItems(at: [self.deletedItemIndex!])
})
}
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
func chunked - is a function that slice array like this (5 items in section):
Before chunked:
[1,2,3,4,5,6,7,8,9,10]
After chunked:
[
[1, 2, 3, 4, 5], // first section in collectionView
[6, 7, 8, 9, 10], // second section in collectionView
]
I populate my items from Core Data to collectionView with this functions:
override func numberOfSections(in collectionView: UICollectionView) -> Int {
print("call numberOfSections")
//3
return items2.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("call numberOfItemsInSection, current section is \(section)")
//4
return items2[section].count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
let item = items2[indexPath.section][indexPath.row]
cell.itemNameTextLabel.text = item.name
cell.itemImageView.image = UIImage(data: item.image! as Data)
return cell
}
}
Item deleting from collectionView and CoreData when user long press it item (cell). Deleting process is here:
#objc func handleLongPress(gesture: UILongPressGestureRecognizer!) {
if gesture.state != .ended {
return
}
let p = gesture.location(in: self.collectionView)
if let indexPath = self.collectionView?.indexPathForItem(at: p) {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %#", self.items2[indexPath.section][indexPath.row].name!)
do {
if let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext {
let selectedItem = try context.fetch(fetchRequest)[0]
//save deleted item index in var that use it index in performBatchUpdatesBlock
deletedItemIndex = IndexPath(row: indexPath.row, section: indexPath.section)
context.delete(selectedItem)
do {
try context.save()
print("Save!")
} catch let error as NSError {
print("Oh, error! \(error), \(error.userInfo)")
}
}
} catch {
print(error.localizedDescription)
}
}
}
Image of this process:
enter image description here
My project like Apple Books app. I want to repeat deleting book process...
My full code is here (GitHub). Plis, use iPhone SE simulator. My data in items.plist file and automatic saving to CoreData when run app in first time.
Where is mistake in my code?
When you delete from the CollectionView, you should also delete from the underlying data array (items2).
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView?.performBatchUpdates({ [weak self] in
guard let self = self else { return }
items = controller.fetchedObjects as! [Item]
items2 = items.chunked(into: 5)
for indexPath in self.deletedItemIndex! {
items2[indexPath.section].remove(at: indexPath.row)
}
self.collectionView?.deleteItems(at: [self.deletedItemIndex!])
})
}
Maybe, the problem is that you delete one item of section 0, and after deletion the section 0 still with the same number of itens before deletion, because you "redistribute" the itens after each deletion.

Loading large data from Firebase into UITableView

I have a comparatively large dataset (about 1,000 rows) that need to be filled into the UITableView from Firebase. I am also using sections and search functionality in the UITableView.
This is how it looks at the moment with the required functionality:
As you can see, section functionality has been used in UITableView. But the issue that I face currently is extremely slow loading of data. I have localPersistenceEnabled but it still takes about 3-4 seconds to populate all data and the experience is less than pleasant.
How do I fix this? I read about Pagination but I also want to preserve the functionality of sections. For sections, this is the code I'm using:
override func viewWillAppear(_ animated: Bool) {
self.racks.removeAll()
self.sectionals.removeAll()
self.tableView.reloadData()
ref.queryOrdered(byChild: "chamber").observe(.childAdded, with: { (snapshot) -> Void in
// Manage TableView Sections & Rows for Rack Additions
var doesSectionExist = false
let segment = (snapshot.value as! [String : AnyObject])["segment"] as! String
if self.sectionals.contains(segment){
// There's already an index present for this letter.
doesSectionExist = true
var snapshotData = self.racks[segment]
snapshotData?.append(snapshot)
self.racks[segment] = snapshotData
}
else{
self.racks[segment] = [snapshot]
self.sectionals.append(segment)
}
self.allRacks.append(snapshot)
self.sectionals = self.sectionals.sorted()
if !doesSectionExist{
self.tableView.insertSections(IndexSet(integer: self.sectionals.index(of: segment)!), with: .fade)
}
else{
//let rowCount = self.customers[firstChar]?.count
self.tableView.reloadSections(IndexSet(integer: self.sectionals.index(of: segment)!), with: .fade)
//self.tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: rowCount!-1, inSection: self.sectionals.indexOf(firstChar)!)], withRowAnimation: .Fade)
}
})
}
override func numberOfSections(in tableView: UITableView) -> Int {
if searchController.isActive && searchController.searchBar.text != "" {
return 1
}
else{
return sectionals.count
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if searchController.isActive && searchController.searchBar.text != "" {
return "Top Matches"
}
else{
return sectionals[section]
}
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
if searchController.isActive && searchController.searchBar.text != "" {
return nil
}
else{
return sectionals
}
}
So the idea is to provide section index titles for quick access and improve loading time to load such a large dataset.
Thank you.

Invalid update on TableView

Hi friends of StackOverflow.
I have a chat screen on my app and I it perform a insertion and deletion based on the actual size of the an Array. Look this:
func addObject(object: Object?) {
if comments == nil || object == nil || object?.something == nil || object?.anything == nil {
return
}
self.objectsTableView.beginUpdates()
if self.objects!.count == 10 {
self.objects?.removeAtIndex(9)
self.objectsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow : 9, inSection: 0)], withRowAnimation: .Right)
}
self.objects?.insert(object!, atIndex: 0)
self.objectsTableView.insertRowsAtIndexPaths([NSIndexPath(forRow : 0, inSection: 0)], withRowAnimation: .Right)
self.objectsTableView.endUpdates()
}
But after some stress test, the log notify:
Invalid update: invalid number of rows in section 0. The number of
rows contained in an existing section after the update (1) must be
equal to the number of rows contained in that section before the
update (10), plus or minus the number of rows inserted or deleted from
that section (1 inserted, 0 deleted) and plus or minus the number of
rows moved into or out of that section (0 moved in, 0 moved out).
I don't know whats happening, this happens only when the insert of objects is very extreme, like one per 0.2 seconds.
Someone know that I can do?
Model mismatch
The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted)
In plain English for the reasonable man, the UITableView thinks you should have 11 rows:
10 before the update + 1 inserted.
number of rows contained in an existing section after the update (1)
...refers to numberOfRowsInSection is returning 1 for section 0, which indicates that the objects array is out of sync, assuming you use something like below:
override func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return objects.count
}
Use NSFetchedResultsController
A clean solution is to use NSFetchedResultsController to be the interface between your model and the UI. It has well studied boilerplate code and is a great platform to ensure thread safety. Documentation here.
Note:
Neat effect! The cell seems to rotate around to the top.
I could not break it using the Gist you produced, nor scheduling multiple concurrent tests. There must be a rogue access to your Object array.
Demo
This simplified version works. Just hook doPlusAction to a button action and watch it loop:
class TableViewController: UITableViewController {
var objects:[Int] = [0,1,2,3,4]
var insertions = 5
#IBAction func doPlusAction(sender: AnyObject) {
tableView.beginUpdates()
objects.removeAtIndex(4)
tableView.deleteRowsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0)], withRowAnimation: .Right)
objects.insert(insertions++, atIndex: 0)
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Right)
tableView.endUpdates()
let delay = 0.1 * Double(NSEC_PER_SEC) //happens the same with this too, when reach 100-150 items
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) { () -> Void in
self.doPlusAction(self)
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
cell.textLabel!.text = "Cell \(objects[indexPath.row])"
return cell
}
}
Name of the guy that solved the problem: Semaphore
The error still happens, but only with a high size of items on list. I don't know what can be.
The DataSource protocol:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let count = self.objects?.count ?? 0
if self.semaphore != nil && semaphoreCode == BLOCKED_STATE {
dispatch_semaphore_signal(self.semaphore!)
}
return count
}
The method that add object:
func addObject(object: Object?) {
if object == nil {
return
}
if self.semaphore != nil {
let tempSemaCode = dispatch_semaphore_wait(semaphore!, 100000)
if tempSemaCode == BLOCKED_STATE {
self.semaphoreCode = RELEASED_STATE
}
}
if self.objects != nil && semaphoreCode != BLOCKED_STATE {
var needsRemoveLastItem = false
if self.objects!.count == 10 {
self.objects?.removeAtIndex(9)
needsRemoveLastItem = true
}
self.objects?.insert(object!, atIndex: 0)
if self.objects!.count > 0 {
self.objectsTableView.beginUpdates()
if needsRemoveLastItem {
self.objectsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow : 9, inSection: 0)], withRowAnimation: .Right)
}
self.objectsTableView.insertRowsAtIndexPaths([NSIndexPath(forRow : 0, inSection: 0)], withRowAnimation: .Right)
self.objectsTableView.endUpdates()
self.semaphore = dispatch_semaphore_create(BLOCKED_STATE)
}
}
}

Swift tableView Pagination

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)

Resources