Firebase data taking ages to load in tableview swift 3 - ios

I have a tableview that loads data from Firebase, and on load it seems to take ages, around 10-15 seconds before any data is shown in the tableview. it also appears thet the app is frozen while this data is loading.
my function for getting the data is: this is called in viewDidLoad
func getTrackData() {
let result = FIRDatabase.database().reference(withPath: "tracks")
result.observe(.value, with: { snapshot in
var newItems: [newTracks] = []
for item in snapshot.children {
let trackDetails = newTracks(snapshot: item as! FIRDataSnapshot)
newItems.append(trackDetails)
}
self.items = newItems
self.items.sort(by: {$0.distance < $1.distance})
self.tableView.reloadData()
})
}
and my tableview is as follows:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "TrackCell", for: indexPath) as? TrackCell {
let tr: newTracks!
if inSearchMode {
tr = filteredTrack[indexPath.row]
cell.configureCell(track: tr)
} else {
tr = items[indexPath.row]
cell.configureCell(track: tr)
}
cell.configureCell(track: tr)
cell.completion = {
let coordinate = CLLocationCoordinate2DMake(tr.lat,tr.lon)
let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: coordinate,addressDictionary:nil))
mapItem.name = tr.name
mapItem.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey :MKLaunchOptionsDirectionsModeDriving])
return()
}
cell.completion1 = {
let url = URL(string: tr.link)!
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.open(url)
}
return()
}
return cell
} else {
return UITableViewCell()
}
}
My question is, is this code written ok, or is there a better way to do it. I have a concern that my function is getting each item and then reloading the tableview, and this may be slowing things down significantly. My data isnt massive, here is an example.
that shows 1 full record, there are 131 of those records in total.

Figured this out now, I had a calculation for distance in miles from current location being worked out with each set of data being loaded. This significantly slowed down the tableview population.
Just need to figure how to do this calculation now, but maybe after the data has loaded

Related

How to use Firestore query pagination with TableVIew

I am trying to use Firestore pagination with swift TableView. Here is my code which loads the first 4 posts from firestore.
func loadMessages(){
let postDocs = db
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.limit(to: 4)
postDocs.addSnapshotListener { [weak self](querySnapshot, error) in
self?.q.async{
self!.posts = []
guard let snapshot = querySnapshot else {
if let error = error {
print(error)
}
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
let nextDocs = Firestore.firestore()
.collectionGroup("userPosts")
.order(by: "postTime", descending: false)
.start(afterDocument: lastSnapshot)
if let postsTemp = self?.createPost(snapshot){
DispatchQueue.main.async {
self!.posts = postsTemp
self!.tableView.reloadData()
}
}
}
}
}
func createPost(_ snapshot: QuerySnapshot) ->[Post]{
var postsTemp = [Post]()
for doc in snapshot.documents{
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String,
let userName = doc.get(K.FStore.poster) as? String,
let uID = doc.get(K.FStore.userID) as? String,
let postDate = doc.get("postTime") as? String,
let votesForLeft = doc.get("votesForLeft") as? Int,
let votesForRight = doc.get("votesForRight") as? Int,
let endDate = doc.get("endDate") as? Int{
let post = Post(firstImageUrl: firstImage,
secondImageUrl: secondImage,
firstTitle: firstTitle,
secondTitle: secondTitle,
poster: userName,
uid: uID,
postDate: postDate,
votesForLeft: votesForLeft,
votesForRight:votesForRight,
endDate: endDate)
postsTemp.insert(post, at: 0)
}else{
}
}
return postsTemp
}
Here is my delegate which also detects the end of the TableView:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
let seconds = post.endDate
let date = NSDate(timeIntervalSince1970: Double(seconds))
let formatter = DateFormatter()
formatter.dateFormat = "M/d h:mm"
if(seconds <= Int(Date().timeIntervalSince1970)){
cell.timerLabel?.text = "Voting Done!"
}else{
cell.timerLabel?.text = formatter.string(from: date as Date)
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
cell.userName.setTitle(post.poster, for: .normal)
cell.firstImageView.layer.cornerRadius = 8.0
cell.secondImageView.layer.cornerRadius = 8.0
if(indexPath.row + 1 == posts.count){
print("Reached the end")
}
return cell
}
Previously I had an addSnapshotListener without a limit on the Query and just pulled down all posts as they came. However I would like to limit how many posts are being pulled down at a time. I do not know where I should be loading the data into my model. Previously it was being loaded at the end of the addSnapshotListener and I could still do that, but when do I use the next Query? Thank you for any help and please let me know if I can expand on my question any more.
There is a UITableViewDelegate method called tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) that will be called just before a cell is loading.
You could use this one to check if the row at IndexPath is in fact the cell of the last object in your tableview's datasource. Something like datasource.count - 1 == IndexPath.row (The -1 is to account for item 0 being the first item in an array, where as it already counts as 1).
If that object is indeed the last one in your datasource, you could make a call to Firebase and add items to the datasource. Before mutating the datasource, make sure to check the new number of objects the show (the ones already loaded + new ones) has to be larger than the current number of objects in the datasource, otherwise the app will crash.
You also might want to give your user a heads up that you're fetching data. You can trigger that heads up also in the delegate method.

iOS swift app terminating on TableView when scrolled to bottom

I am using Firebase to populate a TableView in my iOS app. The first few objects are loaded but once I get to the third item in my list the app crashes with the exception:
'NSRangeException', reason: '*** __boundsFail: index 3 beyond bounds [0 .. 2]'
I know that this means that I am referring to an array at an index that it does not contain however I do not know why.
I create the TableView with a TableViewController and initialize it like so:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(posts.count)
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
print(post)
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
// Configure the cell...
return cell
}
I believe that the first function creates an array with the number of objects in posts and that the second function assigns values to the template for the cell. The print statement in the first method prints 4 which is the correct number of objects retrieved from firebase. I assume that means an array is created with 4 objects to be displayed in the TableView. This is what is really confusing because the error states that there are only 3 objects in the array. Am I misunderstanding how the TableView is instantiated?
Here is the code that fills the TableView:
func loadMessages(){
db.collectionGroup("userPosts")
.addSnapshotListener { (querySnapshot, error) in
self.posts = []
if let e = error{
print("An error occured trying to get documents. \(e)")
}else{
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
if let firstImage = data[K.FStore.firstImageField] as? String,
let firstTitle = data[K.FStore.firstTitleField] as? String,
let secondImage = data[K.FStore.secondImageField] as? String,
let secondTitle = data[K.FStore.secondTitleField] as? String{
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
self.posts.insert(post, at: 0)
print("Posts: ")
print(self.posts.capacity)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
The app builds and runs and displays the first few items but crashes once I scroll to the bottom of the list. Any help is greatly appreciated.
Edit:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
}
You're getting an out-of-bounds error because you're dangerously populating the datasource. You have to remember that a table view is constantly adding and removing cells as it scrolls which makes updating its datasource a sensitive task. You reload the table on each document iteration and insert a new element in the datasource at index 0. Any scrolling during an update will throw an out-of-bounds error.
Therefore, populate a temporary datasource and hand that off to the actual datasource when it's ready (and then immediately reload the table, leaving no space in between an altered datasource and an active scroll fetching from that datasource).
private var posts = [Post]()
private let q = DispatchQueue(label: "userPosts") // serial queue
private func loadMessages() {
db.collectionGroup("userPosts").addSnapshotListener { [weak self] (snapshot, error) in
self?.q.async { // go into the background (and in serial)
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
return
}
var postsTemp = [Post]() // setup temp collection
for doc in snapshot.documents {
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String {
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
postsTemp.insert(post, at: 0) // populate temp
}
}
DispatchQueue.main.async { // hop back onto the main queue
self?.posts = postsTemp // hand temp off (replace or append)
self?.tableView.reloadData() // reload
}
}
}
}
Beyond this, I would handle this in the background (Firestore returns on the main queue) and only reload the table if the datasource was modified.
After some fiddling around and implementing #bsod's response I was able to get my project running. The solution was in Main.Storyboard under the Attributes inspector I had to set the content to Dynamic Prototypes.

Retrieving image from Firebase Storage placing in tableView not working,

I was able to display the image properly when i have just an imageView in a ViewController. I used this code:
islandRef.getData(maxSize: 1 * 1024 * 1024) { [weak self] data, error in
if let error = error {
print((error.localizedDescription))
// Uh-oh, an error occurred!
}
if let data = data{
self?.imageItem.image = UIImage(data: data)
}
}
}
I try a similar approach for my tableView, but its a little more complicated. I have two arrays that pull a string and an integer from my firebase document. I append these to the arrays items and prices. I am able to show these values in my tableView. I try the same thing when my pictues array. I am able to append an image to it. I then check if the pictures array has a count. It has a count of 1, but when i try to access it in tableview. It says the error: Thread 1: Fatal error: Index out of range. I don't understand why my other arrays have values that are usable, but this array does. I don't think there is a problem with the imageView because i can replaces pictures[indexPath.row] with my PlaceholderImage and it will properly show my placeholder image.
class ProfileViewController: UITableViewController {
var item = [String]()
var prices = [Int]()
var pricePicture = [Any]()
var db:Firestore!
var pictures = [UIImage]()
let storage = Storage.storage()
var placeholderImage = UIImage(named: "placeholder.jpg")
var imageMenu:UIImage?
override func viewDidLoad() {
getData()
let db = Firestore.firestore()
super.viewDidLoad()
}
func getData(){
let db = Firestore.firestore()
let docRef = db.collection("wine").document("pinot-noir-2017")
let storage = Storage.storage()
docRef.getDocument(source: .server) { (document, error) in
if let document = document {
let keys = document.data()?.keys
for key in keys!{
self.item.append(key)
self.pricePicture = document.data()![key] as! [Any]
self.prices.append(self.pricePicture[0] as! Int)
let stor = storage.reference()
let islandRef = stor.child("carbanet.jpg")
islandRef.getData(maxSize: 1 * 1024 * 1024) { [weak self] data, error in
if let error = error {
print((error.localizedDescription))
}
if let data = data{
self?.pictures.append(UIImage(data: data)!)
//this will print 1
print(self?.pictures.count ?? 0)
}
}
self.tableView.reloadData()
}
} else {
//print("Document does not exist in cache")
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return item.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Picture", for: indexPath)
cell.textLabel?.text = item[indexPath.row] + " $" + String(prices[indexPath.row])
cell.imageView?.image = pictures[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let detailViewController = storyboard?.instantiateViewController(identifier: Constants.Storyboard.detailViewController) as? DetailViewController{
show(detailViewController, sender: .none)
}
}
}
If someone could show me the error I am making that would be great. I'm pretty new to swift, so any help you can give me would be appreciated. I have read through the firebase documentation and watched their videos, but I can't figure out why it works in one scenario and not the other.
I think it was because i was trying to do it inside of trying to access a firebase document. I made another function getImage() and this time it worked.
func getImage(){
let stor = storage.reference()
let islandRef = stor.child("carbanet.jpg")
islandRef.getData(maxSize: 1 * 1024 * 1024) { [weak self] data, error in
if let error = error {
print((error.localizedDescription))
// Uh-oh, an error occurred!
}
if let data = data{
self?.pictures.append(UIImage(data: data as Data)!)
//this will print 1
print(self?.pictures.count ?? 0)
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}

Issue trying to complete Firebase Storage download before showing tableview

I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.

UITableview and asynchronous requests

I have an issue where if a user types into the search bar too fast the program will crash with the following message
fatal error: index out of range
referring to the line var podInfo = podcastResults[row] which is part of the cellForRowAtIndexPath method. The search box is above a UITableView which is populated from the NSURLSession results.
Please see the code below.
class SearchTVC: UITableViewController, UISearchBarDelegate {
#IBOutlet weak var searchBar: UISearchBar!
var podcastResults = [[String: String]]()
var tempDict = [String: String]()
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
print("search being typed")
if searchText.characters.count >= 3 {
let searchesArray:Array = searchText.componentsSeparatedByString(" ")
//request search method to start
search(searchesArray)
}
}
func search(searchqueries: Array<String>){
let URL = iTunesSearcher().searchQuery(searchqueries) //This just complies the URL using a method in anothr class
let task = NSURLSession.sharedSession().dataTaskWithURL(URL) {
(data, response, error) in
print("URL downloaded")
//clear results and temp dict, so that new results can be displayed
self.tempDict.removeAll()
self.podcastResults.removeAll()
let data = NSData(contentsOfURL: URL) //urlString!
let json = JSON(data: data!)
for (key, subJson) in json["results"] {
if let title = subJson["collectionCensoredName"].string {
self.tempDict = ["title": title]
} else { print("JSON - no title found") }
if let feedURL = subJson["feedUrl"].string {
self.tempDict.updateValue(feedURL, forKey: "feedURL")
} else { print("JSON - no feedURL found") }
if let artworkUrl60 = subJson["artworkUrl60"].string {
self.tempDict.updateValue(artworkUrl60, forKey:"artworkURL60")
} else { print("JSON - no artwork url found") }
self.podcastResults.append(self.tempDict)
}
//Running request on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
task.resume()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath)
let row = indexPath.row
var podInfo = podcastResults[row]
cell.textLabel?.text = podInfo["title"]
return cell
}
Any help would be much appreciated as I just can't figure it out.
Cheers.
Michael
I'm assuming that the number of rows you return in your UITableViewDataSource is self.podcastResults.count.
If so, what you need to do is turn this:
let row = indexPath.row
var podInfo = podcastResults[row]
cell.textLabel?.text = podInfo["title"]
into
let row = indexPath.row
if row < podcastResults.count {
var podInfo = podcastResults[row]
cell.textLabel?.text = podInfo["title"]
}
This will ensure that no matter when the cell is requested the index will never be out of bounds (and I think this happens after you remove all the elements from the array in the request handler).
Try reloading the table when you remove all the elements from your array ie. self.tempDict.removeAll()
self.podcastResults.removeAll() it seems that table is not refreshed and still shows the elements which are now actually removed.

Resources