'Array Index out of Range' on 'RefreshControl' - ios

I have a TableView where I am fetching data from api. Everything working fine with fetching/displaying data and pagination/infinite-scroll. However, when I try to use my RefreshControl, it crashes with an error:
Array index out of range
var theRefreshControl: UIRefreshControl!
var results: [JSON]! = []
// more stuff..
func refresh(sender: AnyObject) {
if isFirstLoad == false {
self.results = []
currentPage = 1
getPosts()
}
self.theRefreshControl.endRefreshing()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : PostCell = tableView.dequeueReusableCellWithIdentifier("PostCell") as! PostCell
cell.delegate = self
print(indexPath.row) // check below.
let post = self.results![indexPath.row] // 'array index out of range' here.
}
To get more detail about the error, I tried adding print(indexPath.row). What I receive is, I receive from 0 to 10 in first load, and then as soon as I refresh, it gets weird:
Also, when I click on the (i):
{length = 2, path = 0 - 9}
Interestingly, exactly same approach works in my other TableViewControllers, only this one is bugging. What may be the problem?
func getPosts() {
isLoading = true
Alamofire.request(.GET, link & params: ["page":"\(currentPage ?? 1)"])
.responseJSON { response in
// error checks
if let data = json["data"].arrayValue as [JSON]? {
self.lastPage = json["last_page"].int!
self.currentPage = json["current_page"].int!
if self.currentPage == 1 {
self.results = data
self.tableView.reloadData()
} else {
var currentCount = self.results!.count;
var indxesPath : [NSIndexPath] = [NSIndexPath]()
for result in data {
indxesPath.append(NSIndexPath(forRow:currentCount,inSection:0));
self.results?.append(result)
currentCount++
}
self.tableView.insertRowsAtIndexPaths(indxesPath, withRowAnimation: UITableViewRowAnimation.Bottom)
}
Edit: Please note that I tried results.removeAll() as well as results.removeAll(keepCapacity: false) in refresh button action function, but no luck. I am still getting the same error and same log for `print(indexPath.row) - 0 to 10 in first load, and in refresh, weirdly 6, 7, 8, [], 9

Swift 3.0 - Same issue here, solved in a similar way to #mcclux but with one less step.
Use the inbuilt UIRefreshControl.
var refreshControl: UIRefreshControl!, then within viewDidLoad configure it:
refreshControl = UIRefreshControl.init()
refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh")
refreshControl.addTarget(self, action: #selector(yourViewControllerName.yourFunctionToCallOnRefresh), for: UIControlEvents.valueChanged)
tableView.addSubview(refreshControl)
Then you can wrap your code within cellForRowAt indexPath with
if !self.refreshControl.isRefreshing {
...
}
Worked perfectly for me after struggling through it crashing for hours. Hope this helps!

I had a similar issue; it was caused in my case by tableView being called during the refresh, during the period when the content was being reset. Because the index was empty for a period, the app was occasionally throwing index out of range. It didn't happen every time. I solved it by creating a boolean var (isRefreshing) that gets set to true when the refresh starts and then set to false once the refresh is over. Inside of tableView() I have a wrapper for the functionality. In your case it would look like:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if !self.isRefreshing {
let cell : PostCell = tableView.dequeueReusableCellWithIdentifier("PostCell") as! PostCell
cell.delegate = self
print(indexPath.row) // check below.
let post = self.results![indexPath.row] // 'array index out of range' here.
}
}
Edit: here's how I'm using it. The cell creation and return are outside of the if:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SessionTableViewCell", forIndexPath: indexPath) as! SessionTableViewCell
if !self.isReloading {
...
}
return cell
}

Solved my problem by deletingself.results = [] from refresh() function. Interestingly, if I empty the array before I refresh, it was creating a problem, but if I directly overwrite the data in the array, it works!

I was facing similar issue when the backing array was emptied like what you did w/ self.results = []. In my finding, there are 2 additional ways to solve this issue.
//1. is to reload table view as soon as you empty the array
func refresh(sender: AnyObject) {
if isFirstLoad == false {
self.results = []
self.tableView.reloadData()
currentPage = 1
getPosts()
}
self.theRefreshControl.endRefreshing()
}
//2. is to delay the execution of the code inside the if scope to give enough time for the table view to get up to date on the count of the results array
func refresh(sender: AnyObject) {
if isFirstLoad == false {
self.delayExecutionByMilliseconds(500) {
self.results = []
currentPage = 1
getPosts()
}
}
self.theRefreshControl.endRefreshing()
}
func delayExecutionByMilliseconds(_ delay: Int, for anonFunc: #escaping () -> Void) {
let when = DispatchTime.now() + .milliseconds(delay)
DispatchQueue.main.asyncAfter(deadline: when, execute: anonFunc)
}

In your refresh function, add self.tableView.reloadData() after cleaning your arrays, and before the function getPosts()

Related

Tableview need to be reloaded twice to update the data from textfield?

I have question about the tableView.
Here is my tableView code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tierCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InterestRateTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InterestRateTableViewCell else {
fatalError("The dequed cell is not an instance of InterestRateTableViewCell.")
}
cell.interestRateTextField.delegate = self
cell.rowLabel.text = "\(indexPath.row + 1)."
if let interestText = cell.interestRateTextField.text {
if let interest = Double(interestText){
interestRateArray[indexPath.row] = interest
} else {
interestRateArray[indexPath.row] = nil
}
} else {
interestRateArray[indexPath.row] = nil
}
return cell
}
As you can see, I have the cellForRowAt method to get the value from the textfields in the cell, and assign to my arrays. (I actually have 2 textfields per cell.)
Basically, I let the users input and edit the textfield until they are happy then click this calculate button, which will call the calculation method. In the calculation method I call the "tableView.reloadData()" first to gather data from the textfields before proceed with the actual calculation.
The problem was when I ran the app. I typed values in all the textfields then clicked "calculate", but it showed error like the textfields were still empty. I clicked again, and it worked. It's like I had to reload twice to get things going.
Can anyone help me out?
By the way, please excuse my English. I'm not from the country that speak English.
edited: It may be useful to post the calculate button code here as someone suggested. So, here is the code of calculate button
#IBAction func calculateRepayment(_ sender: UIButton) {
//Reload data to get the lastest interest rate and duration values
DispatchQueue.main.async {
self.interestRateTableView.reloadData()
}
//Get the loan value from the text field
if let loanText = loanTextField.text {
if let loanValue = Double(loanText) {
loan = loanValue
} else {
print("Can not convert loan value to type Double.")
return
}
} else {
print("Loan value is nil")
return
}
tiers = []
var index = 0
var tier: Tier
for _ in 0..<tierCount {
if let interestRateValue = interestRateArray[index] {
if let durationValue = durationArrayInMonth[index] {
tier = Tier(interestRateInYear: interestRateValue, tierInMonth: durationValue)
tiers.append(tier)
index += 1
} else {
print("Duration array contain nil")
return
}
} else {
print("Interest rate array contain nil")
return
}
}
let calculator = Calculator()
repayment = calculator.calculateRepayment(tiers: tiers, loan: loan!)
if let repaymentValue = repayment {
repaymentLabel.text = "\(repaymentValue)"
totalRepaymentLabel.text = "\(repaymentValue * Double(termInYear!) * 12)"
} else {
repaymentLabel.text = "Error Calculating"
totalRepaymentLabel.text = ""
}
}
cellForRowAt is used for initially creating and configuring each cell, so the textfields are empty when this method is called.
UITableView.reloadData() documentation:
// Reloads everything from scratch. Redisplays visible rows. Note that this will cause any existing drop placeholder rows to be removed.
open func reloadData()
As it says in Apple's comment above, UITableView.reloadData() will reload everything from scratch. That includes your text fields.
There are a number of ways to fix your issue, but it's hard to say the best way without more context. Here's an example that would fit the current context of your code fairly closely:
class MyCustomTableViewCell: UITableViewCell {
#IBOutlet weak var interestRateTextField: UITextField!
var interestRateChangedHandler: (() -> ()) = nil
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
interestRateTextField.addTarget(self, action: #selector(interestRateChanged), for: UIControlEvents.editingChanged)
}
#objc
func interestRateChanged() {
interestRateChangedHandler?()
}
}
and cellForRowAtIndex:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InterestRateTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InterestRateTableViewCell else {
fatalError("The dequed cell is not an instance of InterestRateTableViewCell.")
}
cell.rowLabel.text = "\(indexPath.row + 1)."
cell.interestRateChangedHandler = { [weak self] in
if let interestText = cell.interestRateTextField.text {
if let interest = Double(interestText){
self?.interestRateArray[indexPath.row] = interest
} else {
self?.interestRateArray[indexPath.row] = nil
}
} else {
self?.interestRateArray[indexPath.row] = nil
}
}
return cell
}

EKEvents not populating tableview in swift

I’m new to coding and swift. I’m trying to make a simple appointment app to help with my medical planning. This is my first attempt at a stack overflow question :s, I’m coding in Swift. I’ve managed to get the app access to the Event store and I’ve been able to print all of the events from all calendars into the console, but I can’t get the events to show in my table. I’ve tried to find if anyone else has had this problem on here but I still haven't been able to find a solution that worked in my project. Sorry if this has been asked else where.
I’ve added my code that I have below. I’ve checked all of my ViewController connections and made sure the TableViewCell had an identifier “cell” as well as checking the delegate and datasource connections were attached.
In the function tableView, numberOfRowsInSection, my return self.events.count is returning 0 when I print it to the console. Also my cellForRowAtIndexPath doesn't appear to be called..would this be why nothing is showing in the table? I’m a complete rookie at this so I hope some of this makes sense, I could be completely off the ball…
I really appreciate any help and guidance! Thank you!
class AppointViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var eventStore: EKEventStore!
var events = [EKEvent]()
var startDate = NSDate()
var endDate = NSDate()
var cellIdentifier = "cell"
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
isAppAlreadyLaunchedOnce()
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier);
self.tableView.reloadData();
}
override func viewWillAppear(animated: Bool) {
// Fetch all events
// Connect to the Event Store
self.eventStore = EKEventStore()
self.events = [EKEvent]()
self.eventStore.requestAccessToEntityType(EKEntityType.Event) { (granted: Bool, error: NSError? ) -> Void in
if granted {
print("Access Granted")
let fourYearsAgo = NSDate(timeIntervalSinceNow: -1 * 60 * 60 * 24 * 365 * 2)
let predicate = self.eventStore.predicateForEventsWithStartDate(fourYearsAgo, endDate: self.endDate, calendars: nil)
let events = NSMutableArray(array: self.eventStore.eventsMatchingPredicate(predicate))
//TEST TO MAKE SURE EVENTS ARE BEING EXTRACTED
print(events, "printing events")
dispatch_async(dispatch_get_main_queue()) {
dispatch_async(dispatch_get_global_queue(0, 0)) {
self.eventStore.enumerateEventsMatchingPredicate(predicate) {
(events:EKEvent, stop:UnsafeMutablePointer<ObjCBool>) in
if events.title.rangeOfString("events") != nil {
print("String not nil")
stop .memory = true
}
}
}
}
} else {
print("The app is not permitted to access events")
}
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
//MARK: UITableViewDataSource
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//TEST PRINT EVENT COUNT
print("event count (print) \(events.count)")
return events.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "cell"
let cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
let events:EKEvent! = self.events[indexPath.row]
cell.textLabel!.text = events.title
cell.detailTextLabel!.text = events.startDate.description
print("event cell returned")
return cell
}
}
You didn't call:
self.events = events
self.tableView.reloadData()
After you get the events from store. You should call this on main queue.

Reloading table causes flickering

I have a search bar and a table view under it. When I search for something a network call is made and 10 items are added to an array to populate the table. When I scroll to the bottom of the table, another network call is made for another 10 items, so now there is 20 items in the array... this could go on because it's an infinite scroll similar to Facebook's news feed.
Every time I make a network call, I also call self.tableView.reloadData() on the main thread. Since each cell has an image, you can see flickering - the cell images flash white.
I tried implementing this solution but I don't know where to put it in my code or how to. My code is Swift and that is Objective-C.
Any thoughts?
Update To Question 1
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(R.reuseIdentifier.searchCell.identifier, forIndexPath: indexPath) as! CustomTableViewCell
let book = booksArrayFromNetworkCall[indexPath.row]
// Set dynamic text
cell.titleLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.authorsLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote)
// Update title
cell.titleLabel.text = book.title
// Update authors
cell.authorsLabel.text = book.authors
/*
- Getting the CoverImage is done asynchronously to stop choppiness of tableview.
- I also added the Title and Author inside of this call, even though it is not
necessary because there was a problem if it was outside: the first time a user
presses Search, the call for the CoverImage was too slow and only the Title
and Author were displaying.
*/
Book.convertURLToImagesAsynchronouslyAndUpdateCells(book, cell: cell, task: task)
return cell
}
cellForRowAtIndexPath uses this method inside it:
class func convertURLToImagesAsynchronouslyAndUpdateCells(bookObject: Book, cell: CustomTableViewCell, var task: NSURLSessionDataTask?) {
guard let coverImageURLString = bookObject.coverImageURLString, url = NSURL(string: coverImageURLString) else {
return
}
// Asynchronous work being done here.
task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { (data, response, error) -> Void in
dispatch_async(dispatch_get_main_queue(), {
// Update cover image with data
guard let data = data else {
return
}
// Create an image object from our data
let coverImage = UIImage(data: data)
cell.coverImageView.image = coverImage
})
})
task?.resume()
}
When I scroll to the bottom of the table, I detect if I reach the bottom with willDisplayCell. If it is the bottom, then I make the same network call again.
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == booksArrayFromNetworkCall.count {
// Make network calls when we scroll to the bottom of the table.
refreshItems(currentIndexCount)
}
}
This is the network call code. It is called for the first time when I press Enter on the search bar, then it is called everytime I reach the bottom of the cell as you can see in willDisplayCell.
func refreshItems(index: Int) {
// Make to network call to Google Books
GoogleBooksClient.getBooksFromGoogleBooks(self.searchBar.text!, startIndex: index) { (books, error) -> Void in
guard let books = books else {
return
}
self.footerView.hidden = false
self.currentIndexCount += 10
self.booksArrayFromNetworkCall += books
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
If only the image flash white, and the text next to it doesn't, maybe when you call reloadData() the image is downloaded again from the source, which causes the flash. In this case you may need to save the images in cache.
I would recommend to use SDWebImage to cache images and download asynchronously. It is very simple and I use it in most of my projects. To confirm that this is the case, just add a static image from your assets to the cell instead of calling convertURLToImagesAsynchronouslyAndUpdateCells, and you will see that it will not flash again.
I dont' program in Swift but I see it is as simple as cell.imageView.sd_setImageWithURL(myImageURL). And it's done!
Here's an example of infinite scroll using insertRowsAtIndexPaths(_:withRowAnimation:)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var dataSource = [String]()
var currentStartIndex = 0
// We use this to only fire one fetch request (not multiple) when we scroll to the bottom.
var isLoading = false
override func viewDidLoad() {
super.viewDidLoad()
// Load the first batch of items.
loadNextItems()
}
// Loads the next 20 items using the current start index to know from where to start the next fetch.
func loadNextItems() {
MyFakeDataSource().fetchItems(currentStartIndex, callback: { fetchedItems in
self.dataSource += fetchedItems // Append the fetched items to the existing items.
self.tableView.beginUpdates()
var indexPathsToInsert = [NSIndexPath]()
for i in self.currentStartIndex..<self.currentStartIndex + 20 {
indexPathsToInsert.append(NSIndexPath(forRow: i, inSection: 0))
}
self.tableView.insertRowsAtIndexPaths(indexPathsToInsert, withRowAnimation: .Bottom)
self.tableView.endUpdates()
self.isLoading = false
// The currentStartIndex must point to next index.
self.currentStartIndex = self.dataSource.count
})
}
// #MARK: - Table View Data Source Methods
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = dataSource[indexPath.row]
return cell
}
// #MARK: - Table View Delegate Methods
func scrollViewDidScroll(scrollView: UIScrollView) {
if isLoading == false && scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height {
isLoading = true
loadNextItems()
}
}
}
MyFakeDataSource is irrelevant, it's could be your GoogleBooksClient.getBooksFromGoogleBooks, or whatever data source you're using.
Try to change table alpha value before and after calling [tableView reloadData] method..Like
dispatch_async(dispatch_get_main_queue()) {
self.aTable.alpha = 0.4f;
self.tableView.reloadData()
[self.aTable.alpha = 1.0f;
}
I have used same approach in UIWebView reloading..its worked for me.

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)

iOS Swift, Update UITableView custom cell label outside of tableview CellForRow using tag

Setup (Swift 1.2 / iOS 8.4):
I have UITableView custom cell (identifier = Cell) inside UIViewController. Have two buttons (increment/decrement count) and a label (display count) inside the custom TableView cell.
Goal:
Update the label as we press the increase count or decrease count button.
At present I am able to get the button Tag and call a function outside of the CellForRowAtIndexPath. The button press increases and decreases the count. But I am not able to display the count update in the label.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:FoodTypeTableViewCell = self.tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! FoodTypeTableViewCell
cell.addBtn.tag = indexPath.row // Button 1
cell.addBtn.addTarget(self, action: "addBtn:", forControlEvents: .TouchUpInside)
cell.subBtn.tag = indexPath.row // Button 2
cell.subBtn.addTarget(self, action: "subBtn:", forControlEvents: .TouchUpInside)
cell.countLabel.text = // How can I update this label
return cell
}
func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
count = 1 + count
println(count)
return count
}
func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
println(count)
return count
}
I have seen this question here and there but was not able to find a clear answer in Swift. I would really appreciate if you could help answer it clearly so that other people can not just copy, but clearly understand what is going on.
Thank you.
Here is a solution that doesn't require tags. I'm not going to recreate the cell exactly as you want, but this covers the part you are asking about.
Using Swift 2 as I don't have Xcode 6.x anymore.
Let's start with the UITableViewCell subclass. This is just a dumb container for a label that has two buttons on it. The cell doesn't actually perform any specific button actions, it just passes on the call to closures that are provided in the configuration method. This is part of MVC. The view doesn't interact with the model, just the controller. And the controller provides the closures.
import UIKit
typealias ButtonHandler = (Cell) -> Void
class Cell: UITableViewCell {
#IBOutlet private var label: UILabel!
#IBOutlet private var addButton: UIButton!
#IBOutlet private var subtractButton: UIButton!
var incrementHandler: ButtonHandler?
var decrementHandler: ButtonHandler?
func configureWithValue(value: UInt, incrementHandler: ButtonHandler?, decrementHandler: ButtonHandler?) {
label.text = String(value)
self.incrementHandler = incrementHandler
self.decrementHandler = decrementHandler
}
#IBAction func increment(sender: UIButton) {
incrementHandler?(self)
}
#IBAction func decrement(sender: UIButton) {
decrementHandler?(self)
}
}
Now the controller is just as simple
import UIKit
class ViewController: UITableViewController {
var data: [UInt] = Array(count: 20, repeatedValue: 0)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! Cell
cell.configureWithValue(data[indexPath.row], incrementHandler: incrementHandler(), decrementHandler: decrementHandler())
return cell
}
private func incrementHandler() -> ButtonHandler {
return { [unowned self] cell in
guard let row = self.tableView.indexPathForCell(cell)?.row else { return }
self.data[row] = self.data[row] + UInt(1)
self.reloadCellAtRow(row)
}
}
private func decrementHandler() -> ButtonHandler {
return { [unowned self] cell in
guard
let row = self.tableView.indexPathForCell(cell)?.row
where self.data[row] > 0
else { return }
self.data[row] = self.data[row] - UInt(1)
self.reloadCellAtRow(row)
}
}
private func reloadCellAtRow(row: Int) {
let indexPath = NSIndexPath(forRow: row, inSection: 0)
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.endUpdates()
}
}
When the cell is dequeued, it configures the cell with the value to show in the label and provides the closures that handle the button actions. These controllers are what interact with the model to increment and decrement the values that are being displayed. After changing the model, it reloads the changed cell in the tableview.
The closure methods take a single parameter, a reference to the cell, and from this it can find the row of the cell. This is a lot more de-coupled than using tags, which are a very brittle solution to knowing the index of a cell in a tableview.
You can download a full working example (Requires Xcode7) from https://bitbucket.org/abizern/so-32931731/get/ce31699d92a5.zip
I have never seen anything like this before so I am not sure if this will be the correct way to do. But I got the intended functionality using the bellow code:
For people who find it difficult to understand:
The only problem we have in this is to refer to the TableView Cell. Once you figure out a way to refer the cell, you can interact with the cell components.
func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0) // This defines what indexPath is which is used later to define a cell
let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell! // This is where the magic happens - reference to the cell
count = 1 + count
println(count)
cell.countLabel.text = "\(count)" // Once you have the reference to the cell, just use the traditional way of setting up the objects inside the cell.
return count
}
func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell!
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
cell.countLabel.text = "\(count)"
println(count)
return count
}
I hope someone will benefit from this.
PLEASE CORRECT ME IF THERE IS SOME PROBLEM IN THIS SOLUTION OR THERE IS A BETTER/PROPER WAY TO DO THIS.
Use tableView.reloadData() to reload your tableView content each time you click a button.
let text = "something"
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:FoodTypeTableViewCell = self.tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! FoodTypeTableViewCell
cell.addBtn.tag = indexPath.row // Button 1
cell.addBtn.addTarget(self, action: "addBtn:", forControlEvents: .TouchUpInside)
cell.subBtn.tag = indexPath.row // Button 2
cell.subBtn.addTarget(self, action: "subBtn:", forControlEvents: .TouchUpInside)
cell.countLabel.text = something
return cell
}
func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
count = 1 + count
println(count)
something = "\(count)"
self.tableView.reloadData()
return count
}
func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
println(count)
something = "\(count)"
self.tableView.reloadData()
return count
}
Update1
After your comments ...
you have an array (one value for each food) like this, and whenever you click on a button, you take the index of the row the contains that button, then use that index to retrive the value of count from your array, then reload the table view content.

Resources