UITableView got displayed before data from API using Codable - ios

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.

Related

Firebase Firestore Pagination with Swift

With my app i tried to paginate my data (10 posts per page) from Firestore using below code,
import UIKit
import FirebaseFirestore
class Home: UITableViewController {
var postArray = [postObject]()
let db = Firestore.firestore()
var page : DocumentSnapshot? = nil
let pagingSpinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
override func viewDidLoad() {
super.viewDidLoad()
loadFirstPage()
}
func loadFirstPage(){
// Get the first 10 posts
db.collection("POSTS").limit(to: 10).addSnapshotListener { (snapshot, error) in
if snapshot != nil {
self.postArray = (snapshot?.documents.flatMap({postObject(dec : $0.data())}))!
// Save the last Document
self.page = snapshot?.documents.last
self.tableView.reloadData()
}
}
}
func loadNextPage(){
// get the next 10 posts
db.collection("POSTS").limit(to: 10).start(afterDocument: page!).addSnapshotListener { (snapshot, error) in
if snapshot != nil {
for doc in (snapshot?.documents)! {
self.postArray.append(postObject(dec: doc.data()))
}
self.page = snapshot?.documents.last
self.tableView.reloadData()
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as? postCell
// display data
cell?.textLabel?.text = postArray[indexPath.row].name
return cell!
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// check index to load next page
if indexPath.row < (self.postArray.count){
pagingSpinner.startAnimating()
pagingSpinner.color = UIColor.red
pagingSpinner.hidesWhenStopped = true
tableView.tableFooterView = pagingSpinner
loadNextPage()
}
}
}
But i have faced the following issues :
if i start to post something for the first time (FireStore has no
data at all) from other devices the app will crash because the
page will always be nil.
I tried to insert 10 post by the console and check the app when I
start scrolling down with my table view it will crash for the same
reason page is nil.
I'm wondering why is this happening although I'm saving the last Sanpshot document as pagination cursor ! is there a better why to implement the pagination with Swift
Paginating in Firestore with Swift is very straightforward if we get documents manually (using getDocuments) and not automatically (using addSnapshotListener).
I think it's wise to split the loading of data (the first page) from the continuing of data (the additional pages) for readability. firestoreQuery is a Query object that you must obviously construct on your own.
class SomeViewController: UIViewController {
private var cursor: DocumentSnapshot?
private let pageSize = 10 // use this for the document-limit value in the query
private var dataMayContinue = true
/* This method grabs the first page of documents. */
private func loadData() {
firestoreQuery.getDocuments(completion: { (snapshot, error) in
...
/* At some point after you've unwrapped the snapshot,
manage the cursor. */
if snapshot.count < pageSize {
/* This return had less than 10 documents, therefore
there are no more possible documents to fetch and
thus there is no cursor. */
self.cursor = nil
} else {
/* This return had at least 10 documents, therefore
there may be more documents to fetch which makes
the last document in this snapshot the cursor. */
self.cursor = snapshot.documents.last
}
...
})
}
/* This method continues to paginate documents. */
private func continueData() {
guard dataMayContinue,
let cursor = cursor else {
return
}
dataMayContinue = false /* Because scrolling to bottom will cause this method to be called
in rapid succession, use a boolean flag to limit this method
to one call. */
firestoreQuery.start(afterDocument: cursor).getDocuments(completion: { (snapshot, error) in
...
/* Always update the cursor whenever Firestore returns
whether it's loading data or continuing data. */
if snapshot.count < self.pageSize {
self.cursor = nil
} else {
self.cursor = snapshot.documents.last
}
...
/* Whenever we exit this method, reset dataMayContinue to true. */
})
}
}
/* Let's assume you paginate with infinite scroll which means continuing data
when the user scrolls to the bottom of the table or collection view. */
extension SomeViewController {
/* Standard scroll-view delegate */
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentSize = scrollView.contentSize.height
if contentSize - scrollView.contentOffset.y <= scrollView.bounds.height {
didScrollToBottom()
}
}
private func didScrollToBottom() {
continueData()
}
}
We can still paginate with a snapshot listener but it requires more steps. That is because when a snapshot listener returns, it will return a single page of documents, and if the user has paginated through multiple pages then the update will reset the user back to a single page. The remedy is to keep track of how many pages are rendered on screen and load that many underneath the user when a snapshot listener returns before refreshing the UI. There are additional steps beyond this, however, such as handling a new snapshot return in the middle of a UI refresh; this race condition requires stringent serialization, which may or may not require a semaphore or something as effective.

Data in the UITableView get duplicated when back button is pressed

I working on a project that is written in swift 3.0. My requirement is to save data (on CoreData) that I enter on some text fields and populate one of those attributes in to a table view, thus once a row is selected I wants to update that record (re-assign values on my text fields and save).
Basically I have an entity named "Task" and it got three attributes, and I wants to populate one of those attributes(called "name") that I have saved on core data, in to a table view. Hence when I wants to edit the data that I entered, I tap on a row and it'll direct me to the ViewController where I initially entered those data. However when I click the back button without saving the data it'll duplicate the array and populate in my table view. how can I stop this. The code of the table view class as follow.
import UIKit
import CoreData
class TableViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var stores = [Store] ()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.reloadData()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
let request = NSFetchRequest <NSFetchRequestResult> (entityName: "Store")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request) as! [Store]
// check data existance
if results.count>0 {
print(results.count)
for resultGot in results {
if let expName = resultGot.name {
print("expence name is :", expName)
stores += [resultGot]
print("my array is : \(stores)")
}
}
self.tableView.reloadData()
}
}catch{
print("No Data to load")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stores.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell ()
let store = stores [indexPath.row]
cell.textLabel?.text = store.name
//cell.textLabel?.text = myExpensesArray[indexPath.row]
return cell
}
#IBAction func nextButtonPressed(_ sender: AnyObject) {
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "editStore", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editStore"{
let v = segue.destination as! ViewController
let indexPath = self.tableView.indexPathForSelectedRow
let row = indexPath?.row
v.store = stores[row!]
}
}
This is happening because already loaded elements are present inside your array. When you came back to previously loaded ViewController its method viewWillAppear, viewDidAppear called everytime according to the viewController's life cycle.
You need to clear your previously loaded array using removeAll() method when you came back.
Use below code:
override func viewDidAppear(_ animated: Bool) {
stores.removeAll() // clears all element
let request = NSFetchRequest <NSFetchRequestResult> (entityName: "Store")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request) as! [Store]
// check data existance
if results.count>0 {
print(results.count)
for resultGot in results {
if let expName = resultGot.name {
print("expence name is :", expName)
stores += [resultGot]
print("my array is : \(stores)")
}
}
self.tableView.reloadData()
}
}catch{
print("No Data to load")
}
}
You populate your tableView in the viewDidAppear method which is execute everytime the view is shown (either for first time or coming back from a anbother detail view controller).
You either
populate it once by moving the populate code to viewDidLoad
or clean (remove all objects from) the stores before repopulating it, if you need fresh data to be shown. so before for resultGot in results
insert something like
stores = []

How to reload tableView data after data is passed by a Segue

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];.

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.

How to load dependant tableViewCell API data properly?

I've been working on a UITableView that makes 2 API calls for every cell for a while now. It's been working fine. However today I encountered a massive issue. For the first time, there were more cells on the screen than there was loaded cells.
So when I scrolled down my tableView the screen froze for several seconds because it hadn't loaded the last cell.
A problem I've been having trying to load this data properly. Is that my second API call for each cell depends on the first one.
Here's how it's currently set up:
my tableView:
// #tableView
tableView.setTranslatesAutoresizingMaskIntoConstraints(false)
tableView.backgroundColor = UIColor.formulaWhiteColor()
tableView.frame = CGRectMake(0, 0, 0, 0)
tableView.delegate = self
tableView.dataSource = self
self.tableView.tableFooterView = UIView(frame: CGRectZero)
self.tableView.separatorColor = UIColor.clearColor()
tableView.registerClass(mySuggestionsCell.self, forCellReuseIdentifier: "com.Formula.mySuggestionsCell")
tableView.addSubview(refreshControl)
self.view.addSubview(tableView)
When my view Appears I run this function:
func loadSuggestions() {
DNService.suggestionsForSection() { (JSON) -> () in
self.suggestions = JSON["actionable"]
self.tableView.reloadData()
self.refreshControl.endRefreshing()
}
}
(My DNSService is a struct using Alomafire)
struct DNService {
static func suggestionsForSection(response: (JSON) -> ()) {
let urlString = suggestionsURL
Alamofire.request(.GET, urlString).responseJSON { (_, _, data, _) -> Void in
let suggestions = JSON(data ?? [])
response(suggestions)
}
}
}
From there I start setting up the cells:
Number of rows:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return suggestions.count
}
And the actual configuration:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("com.Formula.mySuggestionsCell", forIndexPath: indexPath) as! mySuggestionsCell
let suggestion = suggestions[indexPath.row]
cell.configureWithSuggestion(suggestion)
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
The rest is currently taking place in my UITableViewCell which I'm pretty sure is where I'm going completely wrong in how I'm doing this.
func configureWithSuggestion(suggestion: JSON) {
tickerString = suggestion["ticker"].string!
tickerLabel.text = tickerString
// I'm setting up other things like above, but cut them out as they aren't relevant.
var quoteAPI = NSURL(string: "http://dev.markitondemand.com/Api/v2/Quote/json?symbol=\(tickerString)")
// This is the API that depends on data from the first API.
// This is being called for every cell that's being configured...
var request = NSURLRequest(URL: quoteAPI!)
var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
if data != nil {
var quote = JSON(data: data!)
lastPrice = quote["LastPrice"]
// Again calling more data than just this.
}
lastPriceLabel.text = lastPrice
// I'm also setting up and animating some charts here.
}
How do I best go about retrieving and setting up this API data, when one of the calls depends on the first one?
I ended up making a coreData entity for the data.
Then when the view loads, I call the first api, loop through it, save it to coreData and in the loop I can use the variable to call the second api as well.
Then when I'm loading my cells from coreData instead of making api calls when scrolling down, there's no problem with loading time.

Resources