Related
When I run the application I can see a blank table like the below screenshot loaded for certain milliseconds and then loading the table with actual data.As the items array is having 0 elements at the beginning, numberOfRowsInSection returns 0 and the blank table view is loading. Is it like that?Please help me on this
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
I changed above code to the one below, but same issue exists and in debug mode I found out that the print("Item array is empty") is executing twice, then the blank table view is displaying for a fraction of seconds, after that the actual API call is happening and data is correctly displayed in the tableview
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if items.isEmpty{
print("Item array is empty")
return 0
} else {
return self.items.count
}
}
import UIKit
class MainVC: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var bookslideShow: UIView!
#IBOutlet weak var bookTableView: UITableView!
var items : [Items] = []
override func viewDidLoad() {
super.viewDidLoad()
bookTableView.dataSource = self
bookTableView.delegate = self
self.view.backgroundColor = UIColor.lightGray
bookTableView.rowHeight = 150
// self.view.backgroundColor = UIColor(patternImage: UIImage(named: "background.jpeg")!)
self.fetchBooks { data in
self.items.self = data
DispatchQueue.main.async {
self.bookTableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if items.isEmpty{
print("Item array is empty")
return 0
} else {
return self.items.count
//bookTableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell",for:indexPath) as! BookCell
//cell.backgroundColor = UIColor(red: 180, green: 254, blue: 232, alpha: 1.00)
let info = items[indexPath.row].volumeInfo
cell.bookTitle.text = info.title
cell.bookCategory.text = info.categories?.joined(separator: ",")
cell.bookAuthor.text = info.authors?.joined(separator: ", ")
let imageString = (info.imageLinks?.thumbnail)!
if let data = try? Data(contentsOf: imageString) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
cell.bookImage.image = image
}
}
}
return cell
}
func fetchBooks(comp : #escaping ([Items])->()){
let urlString = "https://www.googleapis.com/books/v1/volumes?q=quilting"
let url = URL(string: urlString)
guard url != nil else {
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { [self] (data, response, error) in
//check for errors
if error == nil && data != nil{
//parse json
do {
let result = try JSONDecoder().decode(Book.self, from: data!)
comp(result.items)
}
catch {
print("Error in json parcing\(error)")
}
}
}
//make api call
dataTask.resume()
}
}
The delegates methods may be called multiple times. If you want to remove those empty cells initially. You can add this in viewDidLoad:
bookTableView.tableFooterView = UIView()
You are crossing the network for the data. That can take a long time especially if the connection is slow. An empty tableview isn't necessarily bad if you are waiting on the network as long as the user understands what's going on. Couple of solutions,
Fetch the data early in application launch and store locally. The problem with this approach is that the user may not ever need the downloaded resources. For instance if instantgram did that it would be a huge download that wasn't needed for the user. If you know the resource is going to be used entirely get it early or at least a small part of it that you know will be used.
2)Start fetching it early even before the segue. In your code you need it for the table view but you're waiting all the way until view did load. This is pretty late in the lifecycle.
3)If you have to have the user wait on a resource let them know you're loading. Table View has a refresh control that you can call while you are waiting on the network or use a progress indicator or spinner. You can even hide your whole view and present a view so the user knows what's going on.
Also tableview is calling the datasource when it loads automatically and you're calling it when you say reloadData() in your code, that's why you get two calls.
So to answer your question this can be accomplished any number of ways, you could create a protocol or a local copy of the objects instance ie: MainVC in your presentingViewController then move your fetch code to there and set items on the local copy when the fetch comes back. And just add a didset to items variable to reload the tableview when the variable gets set. Or you could in theory at least perform the fetch block in the segue passing the MainVC items in the block.
For instance
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let vc = segue.destination as MainVC
self.fetchBooks { data in
vc.items.self = data // not sure what the extra self is?
DispatchQueue.main.async {
vc.bookTableView.reloadData()
}
}
}
Since the closure captures a strong pointer you can do it this way.
Normally I will do data task as below code show, please see the comments in code.
// show a spinner to users when data is loading
self.showSpinner()
DispatchQueue.global().async { [weak self] in
// Put your heavy lifting task here,
// get data from some completion handler or whatever
loadData()
// After data is fetched OK, push back to main queue for UI update
DispatchQueue.main.async {
self?.tableView.reloadData()
// remove spinner when data loading is complete
self?.removeSpinner()
}
}
After making sure that you have the reloadData() called, make sure your constraints for labels/images are correct. This makes sure that you're labels/images can be seen within the cell.
I have two table views. One which the user clicks on and one where data is displayed. When the user clicks on a cell in the first table view a query is made to my firebase database and the query is stored in an Array. I then pass the data through a segue. I used a property observer so I know that the variable is being set. By using break points I was able to determine that my variable obtains its value right before the cellForRowAtIndexPath method. I need help displaying the data in my table view. I do not know where to reload the data to get the table view to update with my data. I am using Swift.
EDIT 2: I have solved my problem. I will post my first and second table views so that you can see my solution.
FirstTableView
import UIKit
import Firebase
import FirebaseDatabase
class GenreTableViewController: UITableViewController {
let dataBase = FIRDatabase.database()
var genreArray = ["Drama","Classic,Comic/Graphic novel","Crime/Detective","Fable,Fairy tale","Fantasy","Fiction narrative", "Fiction in verse","Folklore","Historical fiction","Horror","Humour","Legend","Magical realism","Metafiction","Mystery","Mythology","Mythopoeia","Realistic fiction","Science fiction","Short story","Suspense/Thriller","Tall tale","Western,Biography","Autobiography","Essay","Narrative nonfiction/Personal narrative","Memoir","Speech","Textbook","Reference book","Self-help book","Journalism", "Religon"]
var ResultArray: [NSObject] = []
var infoArray:[AnyObject] = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return genreArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
// Configure the cell...
cell.textLabel?.text = genreArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let DestViewController: ResultTableViewController = segue.destinationViewController as! ResultTableViewController
if segue.identifier == "letsGo" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let tappedItem = self.genreArray[indexPath.row]
DestViewController.someString = tappedItem
}
}
}
}
import UIKit
import Firebase
import FirebaseDatabase
class ResultTableViewController: UITableViewController {
let dataBase = FIRDatabase.database()
var SecondResultArray: [FIRDataSnapshot]! = []
var someString: String?{
didSet {
print("I AM A LARGE TEXT")
print(someString)
}
}
override func viewDidLoad() {
let bookRef = dataBase.reference().child("books")
bookRef.queryOrderedByChild("Genre")
.queryEqualToValue(someString)
.observeSingleEventOfType(.Value, withBlock:{ snapshot in
for child in snapshot.children {
self.SecondResultArray.append(child as! FIRDataSnapshot)
//print(self.ResultArray)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
})
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return SecondResultArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath)
// Configure the cell...
let bookSnapShot: FIRDataSnapshot! = self.SecondResultArray[indexPath.row]
let book = bookSnapShot.value as! Dictionary<String, String>
let Author = book["Author"] as String!
let Comment = book["Comment"] as String!
let Genre = book["Genre"] as String!
let User = book["User"] as String!
let title = book["title"] as String!
cell.textLabel?.numberOfLines = 0
cell.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping
cell.textLabel?.text = "Author: " + Author + "\n" + "Comment: " + Comment + "\n" + "Genre: " + Genre + "\n" + "User: " + User + "\n" + "Title: " + title
let photoUrl = book["bookPhoto"], url = NSURL(string:photoUrl!), data = NSData(contentsOfURL: url!)
cell.imageView?.image = UIImage(data: data!)
return cell
}
}
For better context and troubleshooting here is my current code for the tableView which is supposed to display data:
import UIKit
class ResultTableViewController: UITableViewController {
var SecondResultArray: Array<NSObject> = []{
willSet(newVal){
print("The old value was \(SecondResultArray) and the new value is \(newVal)")
}
didSet(oldVal){
print("The old value was \(oldVal) and the new value is \(SecondResultArray)")
self.tableView.reloadData()
}
}
override func viewDidLoad() {
print ("I have this many elements\(SecondResultArray.count)")
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return SecondResultArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath)
cell.textLabel?.text = SecondResultArray[indexPath.row] as? String
return cell
}
}
Edit:
Here is my first table view controller. I have tried using the completion handler, but I can't call it correctly and I am constricted by the fact that my query happens in the didSelectRowAtIndexPath method. Please help.
import UIKit
import Firebase
import FirebaseDatabase
class GenreTableViewController: UITableViewController {
let dataBase = FIRDatabase.database()
var genreArray = ["Drama","Classic,Comic/Graphic novel","Crime/Detective","Fable,Fairy tale","Fantasy","Fiction narrative", "Fiction in verse","Folklore","Historical fiction","Horror","Humour","Legend","Magical realism","Metafiction","Mystery","Mythology","Mythopoeia","Realistic fiction","Science fiction","Short story","Suspense/Thriller","Tall tale","Western,Biography","Autobiography","Essay","Narrative nonfiction/Personal narrative","Memoir","Speech","Textbook","Reference book","Self-help book","Journalism", "Religon"]
var ResultArray: [NSObject] = []
var infoArray:[AnyObject] = []
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return genreArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = genreArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
typealias CompletionHandler = (result:NSObject?, error: NSError?) -> Void
func getData(completionHandeler: CompletionHandler){
let bookRef = self.dataBase.reference().child("books")
let GenreSelector = self.genreArray[indexPath.row]
bookRef.queryOrderedByChild("Genre")
.queryEqualToValue(GenreSelector)
.observeSingleEventOfType(.Value, withBlock:{ snapshot in
for child in snapshot.children {
print("Loading group \((child.key!))")
self.ResultArray.append(child as! NSObject)
}
print(self.ResultArray)
self.performSegueWithIdentifier("letsGo", sender: self)
self.tableView.reloadData()
})
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var DestViewController: ResultTableViewController = segue.destinationViewController as! ResultTableViewController
DestViewController.SecondResultArray = self.ResultArray
}
You can inject the data to the destination viewController in prepareForSegue Method of the first UIViewController and reload your UITableView in viewDidAppear. If you are getting your data asynchronously, have a completionHandler and reload it in the completionHandler. Here is an example.
func fetchDataWithCompletion(response: (NSDictionary?, error:NSError?)-> Void) -> Void {
//make the API call here
}
How about this:
Assume you have an array (myArray) populated from Firebase and stored in the first tableViewController. There's a second tableViewController and a segue connecting them.
We want to be able to tap on an item in the first tableviewController, have the app retrieve detailed data for the item from Firebase (a 'data' node) and display the detailed data in the second tableViewController.
Firebase structure
some_node
child_node_0
data: some detailed data about child_node_0
child_node_1
data: some detailed data about child_node_1
Within the second tableViewContoller:
var passedObject: AnyObject? {
didSet {
self.configView() // Update the view.
}
}
Tapping an item in the first tableView calls the following function
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showListInSecondTable" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let tappedItem = myArray[indexPath.row] as! String
let keyOfTappedItem = tappedItem.firebaseKey //child_node_0 for example
doFirebase(keyOfTappedItem)
}
}
}
and the prepareForSegue then calls the following which loads the data from firebase and when the snapshot returns within the block, it populates the passedObject property in the second tableView
func doFirebase(firebaseKey: String) {
ref = myRootRef.childByAppendingPath("\(firebaseKey)/data")
//if we want the detailed data for child_node_0 this would resolve
// to rootRef/child_node_0/data
ref.observeSingleEventOfType(.Value, { snapshot in
let detailObjectToPass = snapshot.Value["data"] as! NSArray or string etc
let controller = (segue.destinationViewController as! UINavigationController).myViewController as! SecondViewController
controller.passedObject = detailObjectToPass
}
and of course in secondController, setting the passedArray calls didSet and sets up the view, and tells the tableView to reload itself, displaying the passed array.
func configView() {
//set up the view and buttons
self.reloadData()
}
I did this super quick so ignore the typos's. The pattern is correct and satisfies the question. (and eliminates the need for an observer to boot!)
P.S. this is way over coded but I wanted to demonstrate the flow and leveraging the asynchronous call to firebase to load the second tableView when the data was valid within the block.
Try updating your closure to include this:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
Edit:
On second read, you are already using a completion handler, but I think you didn't see it. Let me correct your code above a bit:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let bookRef = self.dataBase.reference().child("books")
let GenreSelector = self.genreArray[indexPath.row]
bookRef.queryOrderedByChild("Genre")
.queryEqualToValue(GenreSelector)
.observeSingleEventOfType(.Value, withBlock:{ snapshot in
// This here is your completion handler code!
// I assume it is called asynchronously once your DB is done
for child in snapshot.children {
print("Loading group \((child.key!))")
self.ResultArray.append(child as! NSObject)
}
print(self.ResultArray)
self.performSegueWithIdentifier("letsGo", sender: self)
// self.tableView.reloadData() // is this really needed
})
}
}
You defined a closure, but simply didn't call it. I don't see a reason for that anyways, assuming the block gets called once the database gives you your results. Am I missing something?
That's a good start already, but I think you didn't entirely get how to use a completion handler in this regard, but of course I may be wrong.
I built on top of user3861282's answer and created a small demo project at my github.
In short: You can do all inter-table-communication in the prepareForSegue: method of your first table view controller. Configure the second table view controller there (via its vars). Not any closures/completion handlers there yet.
Then in the second view controller's viewWillAppear: method, start the loading (including an animation if you want). I suggest something like NSURLSession that already defines a completion handler. In that you work with your data from remote, stop any loading animations and you're good.
If the completion handler must be defined in the first table view controller, you can even set it as a var in the second table view controller. That way you "hand over" the closure, i.e. "piece of code".
Alternatively, you can start animations and remote request in the first table view controller and then performSegueWithIdentifier once that is done. In your question you wrote that you want to load in the second table view controller, however, if I understood you correctly.
Your code above properly defines a closure that expects a completion handler (which is also a closure and so kind of doubles what you want), but you never actually call it somewhere. Nor do you call the completion handler in the closure. See my demo for how it can work.
The project I wrote illustrates just one way to do it (minus animations, not enough time). It also shows how you can define your own function expecting a completion handler, but as I said, the standard remote connections in the framework provide one anyways.
Based on additional code that was added to the post, the issue is a controller variable going out of scope.
So here's the issue
class MyClass {
func setUpVars {
let x = 1
}
func doStuff {
print(x)
}
}
Create a class and attempt to print the value of x
let aClass = MyClass()
aClass.setUpVars
aClass.doStuff
This will print nothing (conceptually) as once setUpVars ended, the 'x' variable went out of scope.
whereas
class MyClass {
var x: Int
func setUpVars {
x = 1
}
func doStuff {
print(x)
}
}
will print the value of x, 1.
So the real solution is that your viewControllers need to 'stay alive' during the duration of your class (or app).
Here's the pattern. In the MasterViewController
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
then in your MasterViewController viewDidLoad (or wherever), create the detailViewController
override func viewDidLoad() {
super.viewDidLoad()
let controllers = split.viewControllers //this is from a splitViewController
self.detailViewController =
controllers[controllers.count-1].topViewController as? DetailViewController
}
and from there you have it... use prepareForSegue to 'send' the data to the detailViewController
Just wanted to have this posted for future reference.
You can reload the TableView with [tableView reloadData];.
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.
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)
Is there a theory about reloading new data by appending old data with new from json?I am having problem with solving that case.I used this infinite uitableview to reload more data from my api json string.My Api give me back the results base on page no.
My json header include "totalCount","currentPage" & "toPage" as it was describe in the below code."totalCount" means the total results that i am going to get.But,it gives me 15 results per page.So,if "totalCount" is 636.I have to go 43 pages(toPage).
Here is my code.
ViewController.swift
import UIKit
class ViewController: UITableViewController,AuctionAPIProtocol{
var currentPage = 1
var currentCount = 0
var toPage = 0
var totalCount = 0
var api : AuctionAPI?
let cellId = "cell"
#IBOutlet var tableViewFooter:MyFooter! //Table Footer which was activity indicator
var items:[AuctionModel] = []//An empty array where items gonna store
var newItems:[AuctionModel] = []
var loading = false // Loading State for activity indicator
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: cellId)
self.tableViewFooter.hidden = true
api = AuctionAPI(delegate: self)
api!.searchAuctionLatestFor("null",lotId: "1",page: "1")
}
//TableView Delegate and Datasource
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier(cellId, forIndexPath: indexPath) as UITableViewCell
let item = items[indexPath.row]
cell.textLabel?.text = item.vendorName.capitalizedString+" "+item.modelName.capitalizedString+" "+item.year
return cell
}
override func scrollViewDidScroll(scrollView: UIScrollView) {
// UITableView only moves in one direction, y axis
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
// Change 10.0 to adjust the distance from bottom
if (maximumOffset - currentOffset) <= 40.0 {
loadSegment(currentPage, count: items.count)
}
}
func loadSegment(currentP:Int, count:Int) {
//println("load segment current page: \(currentPage)")
api = AuctionAPI(delegate: self)
if (!self.loading) {
self.setLoadingState(true)
println("CP\(currentP)")
println("TP\(count)")
if currentPage < toPage{
println("Enter Condition")
var times = 0
api!.searchAuctionLatestFor("null",lotId: "1",page: String(++currentPage))
println("Current Page After API Call : \(currentPage)")
}
else if currentPage > toPage {
setLoadingState(false)
}
}
else{
println("Not Loading")
}
}
// Loading Progress at Table View Footer condition (disabling the table view footer which include loading indicator or not)
func setLoadingState(loading:Bool) {
self.loading = loading
self.tableViewFooter.hidden = !loading
}
func didReceiveAPIResults(results: NSDictionary) {
var resultsArr: NSArray = results["body"] as NSArray
//fix
dispatch_async(dispatch_get_main_queue(), {
self.newItems = AuctionModel.latestWithJSON(resultsArr)
println("Current Auction Items : \(self.newItems.count)")
//println("Page: \(self.currentPage) Items \(self.items)")
if self.currentPage > 1 {
for item:AuctionModel in self.newItems {
self.items.append(item)
}
}
else{
self.items = self.newItems
}
self.currentCount = self.items.count
println("After Auction Items : \(self.currentCount)")
self.tableView?.reloadData()
//Status bar network activity ααα― ααααΊβαααΊ/αα½ααΊα·βαα²α·βαααΊ
//UIApplication.sharedApplication().networkActivityIndicatorVisible = false
self.tableView?.hidden = false
})
}
func doSearchMoreAPIResults(results: JSON){
if results != nil{
totalCount = results["header"]["totalCount"].intValue
currentPage = results["header"]["currentPage"].intValue
toPage = results["header"]["toPage"].intValue
}
println("totalCount : \(totalCount)")
println("currentPage : \(currentPage)")
println("toPage : \(toPage)")
}
func didNotReceiveAPIResults(results: Bool){
var connectionResult : Bool = results
}
}
AuctionAPI.swift
import Foundation
protocol AuctionAPIProtocol{
func didReceiveAPIResults(results: NSDictionary)
func didNotReceiveAPIResults(results: Bool)
func doSearchMoreAPIResults(results:JSON)
}
class AuctionAPI{
var delegate: AuctionAPIProtocol
var urlParameterStringController:URLParameterStringController!
init(delegate: AuctionAPIProtocol){
self.delegate=delegate
}
func post(path:String,params:Dictionary<String,String>){
//Parameter Parts.....
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
.....
self.delegate.didReceiveAPIResults(jsonData)
self.delegate.doSearchMoreAPIResults(json)
})
task.resume()
}
func searchAuctionLatestFor(token:String,lotId:String,page:String){
.....
post(urlPath,params: params)
}
}
I updated the code,and now i am stuck at "Loading" when i scroll down after getting 30 results.Actually it should load 636 results.
This is my console output.
This is my simulator which was at page 2 and stuck at loading.Fig
No more duplicate results now,but why it dont load more data and stuck at page 2 when i scroll down to load more.
The reason why i am stucking at Page 2 with loading was,I dont set the "setLoadingState()" to false after i get the results from "didReceiveAPIResults()".So after appending the results into items Array,doing self.setLoadingState(false) solved the problem and it will enter to if(!self.loading) condition when user pull up to load more data from another page.
Thank you.I will not update the answer because i want all who like me to find out.
In didReceiveAPIResults you are setting self.items and hen appending to it so you are deleting the old data and then duplicating it. You should simply be appending the new results onto the existing list.