I followed the Firebase tutorial by Ray Wenderlich (Link) and adopted his way of initializing the object (in my case of type "Location") with the snapshot from the observe-method:
class Location:
init(snapshot: FIRDataSnapshot) {
identifier = snapshot.key
let snapshotValue = snapshot.value as! [String : AnyObject]
type = snapshotValue["type"] as! String
name = snapshotValue["name"] as! String
address = snapshotValue["address"] as! String
latitude = Double(snapshotValue["latitude"] as! String)!
longitude = Double(snapshotValue["longitude"] as! String)!
avatarPath = snapshotValue["avatarPath"] as! String
ref = snapshot.ref
}
LocationsViewController:
databaseHandle = locationsRef?.queryOrdered(byChild: "name").observe(.value, with: { (snapshot) in
var newLocations:[Location] = []
for loc in snapshot.children {
let location = Location(snapshot: loc as! FIRDataSnapshot)
newLocations.append(location)
}
self.locations = newLocations
self.tableView.reloadData()
})
This really works like a charm, but now I'm trying to load the image stored under the storage reference "avatarPath".
My attempt worked but the images take a ling time to load. Is there a better way/place to load these images?
My attempt 1:
databaseHandle = locationsRef?.queryOrdered(byChild: "name").observe(.value, with: { (snapshot) in
var newLocations:[Location] = []
for loc in snapshot.children {
let location = Location(snapshot: loc as! FIRDataSnapshot)
newLocations.append(location)
}
self.locations = newLocations
self.tableView.reloadData()
//Load images
for loc in self.locations {
let imagesStorageRef = FIRStorage.storage().reference().child(loc.avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
loc.avatarImage = UIImage(data: data!)!
self.tableView.reloadData()
}
})
}
})
My 2nd Attempt (inside Location class):
init(snapshot: FIRDataSnapshot) {
identifier = snapshot.key
let snapshotValue = snapshot.value as! [String : AnyObject]
type = snapshotValue["type"] as! String
name = snapshotValue["name"] as! String
address = snapshotValue["address"] as! String
latitude = Double(snapshotValue["latitude"] as! String)!
longitude = Double(snapshotValue["longitude"] as! String)!
avatarPath = snapshotValue["avatarPath"] as! String
ref = snapshot.ref
super.init()
downloadImage()
}
func downloadImage() {
let imagesStorageRef = FIRStorage.storage().reference().child(self.avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
self.avatarImage = UIImage(data: data!)!
}
})
}
Thank you in advance!
Nico
The best way you can accomplish that is to load asynchronous inside the loading of the cell function. I mean:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
DispatchQueue.main.async {
let imagesStorageRef = FIRStorage.storage().reference().child(self.locations[indexPath.row].avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
locations[indexPath.row].avatarImage = UIImage(data: data!)!
tableView.reloadRows(at indexPaths: [indexPath], with animation: .none)
}
})
}
}
In first attempt try changing your code as:
DispatchQueue.main.async {
for loc in self.locations {
let imagesStorageRef = FIRStorage.storage().reference().child(loc.avatarPath)
imagesStorageRef.data(withMaxSize: 1*1024*1024, completion: { (data, error) in
if let error = error {
print(error.localizedDescription)
} else {
loc.avatarImage = UIImage(data: data!)!
self.tableView.reloadData()
}
})
}
}
Related
I want to load all data from firebase, then show the data to the table view. But now, I can't show all the data to the table view. It is because call the finishLoading(realm) method is faster than the for loop get all the data. How can I do some show all data when for loop is finish in swift. I have to use the Closure, however the second of the loop is later than this "self.finishLoading(realm: realm)"
I have to try to add the DispatchGroup(), however, the leave() when having an error of EXC_BAD_INSTRUCTION. Can I put the leave() in the closure? How can I fix it?
func loopAllProduct(userId: String, finishLoadWhenErr:Bool, storedClosure: #escaping (DocumentSnapshot) -> Void){
let storage = Storage.storage()
let db = Firestore.firestore()
let userDocRef = db.collection("Users").document(userId).collection("Product")
userDocRef.getDocuments{(document, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
for document in document!.documents {
storedClosure(document)
}
}
}
}
func downloadData() {
let startTime = Date()
while updating {
let diffTime = Date(timeIntervalSinceReferenceDate: startTime.timeIntervalSinceReferenceDate)
if (diffTime.timeIntervalSinceNow < -5){
self.stopAnimating()
self.refreshControl?.endRefreshing()
print("Update Timeout")
return
}
}
updating = true
let storage = Storage.storage()
let db = Firestore.firestore()
let productLoading = NSMutableArray()
let realm = try! Realm()
print("all posts")
let group = DispatchGroup()
let addPosts: (DocumentSnapshot)->Void = {(document) in
try! realm.write {
if let resuls = self.realmResults {
realm.delete(resuls);
}
}
let product = Product()
product.id = document.documentID
product.userID = document.data()?["UserID"] as? String
product.userName = document.data()?["UserName"] as? String
product.descrition = document.data()?["Descrition"] as? String
product.postTime = document.data()?["PostTime"] as? Date
product.price = document.data()?["Price"] as? Double ?? 0.0
product.stat = (document.data()?["stat"] as? Int)!
product.productName = document.data()?["ProductName"] as? String
let productId = document.documentID
productLoading.add(productId)
try! realm.write {
realm.add(product)
}
group.leave()
}
let userDocRef = db.collection("Users")
userDocRef.getDocuments{(document, error) in
for document in document!.documents {
group.enter()
self.loopAllProduct(userId:document.documentID , finishLoadWhenErr: true, storedClosure: addPosts)
}
}
group.notify(queue: DispatchQueue.main) {
self.finishLoading(realm: realm)
}
}
Just updated my cocoa pods thus updating firebase. This line of code is the old way, which is wrong now:
let downloadURL = metaData!.downloadURL()!.absoluteString
let values: Dictionary<String, Any> = ["download_url": downloadURL]
The following code is the correct way now to extract the URL string. Yet, I need help on how I can put that string into my array to save to firebase.
storageRef.downloadURL(completion: {(url, error) in
if error != nil {
print(error!.localizedDescription)
return
}
let downloadURL = url?.absoluteString
})
let values: Dictionary<String, Any> = ["download_url": downloadURL]
How I save "values" as a child
let databaseRef = Database.database().reference()
let path = databaseRef.child("posts").child((self.loggedInUser?.uid)!).childByAutoId()
path.setValue(values) { (error, ref) -> Void in
if error != nil {
print("error saving post in db")
} else {
let storageRef = Storage.storage().reference().child("posts_requests").child((self.loggedInUser?.uid)!).child(snapshot.childSnapshot(forPath: "uid").value as! String).child(snapshot.childSnapshot(forPath: "uid").value as! String).child(snapshot.childSnapshot(forPath: "imageID").value as! String)
storageRef.delete(completion: { error in
if let error = error {
print(error)
} else {
print("Successful Delete")
}
})
}
}
Using the answers below...
When I use the submitted answers below I get a print out saying "User does not have permission to access gs://shoppeer-e7270.appspot.com/(null)." All I am trying to accomplish is grabbing that URL string and adding it to my "values" which is a Dictionary.
My full code for image upload as well as saving as a child
let photosRef = storage.reference().child("posts").child((loggedInUser?.uid)!)
let usersRef = Database.database().reference().child("Businesses")
let databaseRef = Database.database().reference()
let imageName = NSUUID().uuidString
let photoRef = photosRef.child("\(uid)")
let postID = databaseRef.child("posts").child((loggedInUser?.uid)!).childByAutoId().key
var downloadURLSting = String()
photoRef.child("\(imageName)").putData(data!, metadata: nil) { (metaData,error) in
if let error = error {
print("there was an error")
print(error.localizedDescription)
return
} else {
// store downloadURL
storage.reference().downloadURL(completion: {(url, error) in
if error != nil {
print(error!.localizedDescription)
return
}
let downloadURL = url?.absoluteString
let values: Dictionary<String, Any> = ["uid": uid, "caption": caption ?? "", "download_url": downloadURL, "timestamp": ServerValue.timestamp(), "businessName":loggedInUserData?["businessName"] as! String, "businessStreet":loggedInUserData?["businessStreet"] as! String, "businessCity":loggedInUserData?["businessCity"] as! String, "businessState":loggedInUserData?["businessState"] as! String, "businessZIP":loggedInUserData?["businessZIP"] as! String, "businessPhone":loggedInUserData?["businessPhone"] as! String, "businessWebsite":loggedInUserData?["businessWebsite"] as! String, "businessLatitude":loggedInUserData?["businessLatitude"] as! String, "businessLongitude":loggedInUserData?["businessLongitude"] as! String, "facebookURL":loggedInUserData?["facebookURL"] as! String, "twitterURL":loggedInUserData?["twitterURL"] as! String, "instagramURL":loggedInUserData?["instagramURL"] as! String, "googleURL":loggedInUserData?["googleURL"] as! String, "yelpURL":loggedInUserData?["yelpURL"] as! String, "foursquareURL":loggedInUserData?["foursquareURL"] as! String, "snapchatURL":loggedInUserData?["snapchatURL"] as! String, "imageID": imageName, "postID": postID]
// store downloadURL at database
let databaseRef = Database.database().reference()
let path = databaseRef.child("posts").child((loggedInUser?.uid)!).childByAutoId()
path.setValue(values) { (error, ref) -> Void in
if error != nil {
print("error saving post in db")
} else {
// reset caption field
self.descriptionTextView.text = ""
// reset placeholder image
self.imageView.image = UIImage(named: "filterPlaceholder")
MBProgressHUD.hide(for: self.view, animated: true)
let viewConrolller = self.storyboard?.instantiateViewController(withIdentifier: "Business Profile") as! UITabBarController
self.present(viewConrolller, animated: true, completion: nil)
}
}
})
}
}
This works just downloadURL string is nil
let photosRef = storage.reference().child("posts").child((loggedInUser?.uid)!)
let usersRef = Database.database().reference().child("Businesses")
let databaseRef = Database.database().reference()
let imageName = NSUUID().uuidString
let photoRef = photosRef.child("\(uid)")
let postID = databaseRef.child("posts").child((loggedInUser?.uid)!).childByAutoId().key
photoRef.child("\(imageName)").putData(data!, metadata: nil) { (metaData,error) in
if let error = error {
print("there was an error")
print(error.localizedDescription)
return
} else {
// store downloadURL
photoRef.downloadURL(completion: {(url, error) in
if error != nil {
guard let downloadURL = url?.absoluteString else { return }
let values: Dictionary<String, Any> = ["uid": uid, "caption": caption ?? "", "download_url": downloadURL, "timestamp": ServerValue.timestamp(), "businessName":loggedInUserData?["businessName"] as! String, "businessStreet":loggedInUserData?["businessStreet"] as! String, "businessCity":loggedInUserData?["businessCity"] as! String, "businessState":loggedInUserData?["businessState"] as! String, "businessZIP":loggedInUserData?["businessZIP"] as! String, "businessPhone":loggedInUserData?["businessPhone"] as! String, "businessWebsite":loggedInUserData?["businessWebsite"] as! String, "businessLatitude":loggedInUserData?["businessLatitude"] as! String, "businessLongitude":loggedInUserData?["businessLongitude"] as! String, "facebookURL":loggedInUserData?["facebookURL"] as! String, "twitterURL":loggedInUserData?["twitterURL"] as! String, "instagramURL":loggedInUserData?["instagramURL"] as! String, "googleURL":loggedInUserData?["googleURL"] as! String, "yelpURL":loggedInUserData?["yelpURL"] as! String, "foursquareURL":loggedInUserData?["foursquareURL"] as! String, "snapchatURL":loggedInUserData?["snapchatURL"] as! String, "imageID": imageName, "postID": postID]
// store downloadURL at database
let databaseRef = Database.database().reference()
let path = databaseRef.child("posts").child((loggedInUser?.uid)!).childByAutoId()
path.setValue(values) { (error, ref) -> Void in
if error != nil {
print("error saving post in db")
} else {
// reset caption field
self.descriptionTextView.text = ""
// reset placeholder image
self.imageView.image = UIImage(named: "filterPlaceholder")
MBProgressHUD.hide(for: self.view, animated: true)
let viewConrolller = self.storyboard?.instantiateViewController(withIdentifier: "Business Profile") as! UITabBarController
self.present(viewConrolller, animated: true, completion: nil)
}
}
} else {
print(error!.localizedDescription)
print("error")
return
}
})
}
}
values is a Dictionary, not an Array, but if you want to add downloadURL to it, you'll need to do that inside the completion handler of storageRef.downloadURL(completion:), since that's an asynchronous method.
storageRef.downloadURL(completion: {(url, error) in
if error != nil {
print(error!.localizedDescription)
return
}
let downloadURL = url?.absoluteString
let values: Dictionary<String, Any> = ["download_url": downloadURL]
})
The download URL is only available inside the completion handler.
storageRef.downloadURL(completion: {(url, error) in
if error != nil {
print(error!.localizedDescription)
return
}
let downloadURL = url?.absoluteString
let values: Dictionary<String, Any> = ["download_url": downloadURL]
let databaseRef = Database.database().reference()
let path = databaseRef.child("posts").child((self.loggedInUser?.uid)!).childByAutoId()
path.setValue(values) { (error, ref) -> Void in
...
I have looked at a lot of questions and I do not believe this is a result of reusing a cell as the new cells image is correct, but and existing cells image is incorrect and used to be correct. I'll post the images first so the issue is easier to understand.
I have a collectionView of image cells (similar to Instagrams user page). I'm fetching all of the data from Firebase. I get the first 12 posts on the initial loading of the screen. However, if you scroll down quickly an EXISTING cells image changes to a newly fetched image. I'm not sure why this is happening... Maybe it's a caching issue? The issue only occurs the first time you load the screen. I've tried setting the images to nil like this:
override func prepareForReuse() {
super.prepareForReuse()
self.imageView.image = UIImage()
}
This didn't help the issue though.
Here is my cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath) as! ImageCell
cell.indexPath = indexPath
cell.imageView.downloadImage(from: currentTablePosts[indexPath.row].pathToImage)
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor.black.cgColor
return cell
}
Image downloading and caching:
let imageCache = NSCache<NSString, UIImage>()
extension UIImageView {
func downloadImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
imageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
}
Firebase Queries:
static func getInitialTablesPosts(tableNumber: String) {
tableReference.child(tableNumber).queryLimited(toLast: 12).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let post = child?.value as? [String: AnyObject] {
let posst = Post()
if let author = post["author"] as? String, let likes = post["likes"] as? Int, let pathToImage = post["pathToImage"] as? String, let postID = post["postID"] as? String, let postDescription = post["postDescription"] as? String, let timestamp = post["timestamp"] as? Double, let category = post["category"] as? String, let table = post["group"] as? String, let userID = post["userID"] as? String, let numberOfComments = post["numberOfComments"] as? Int, let region = post["region"] as? String {
posst.author = author
posst.likes = likes
posst.pathToImage = pathToImage
posst.postID = postID
posst.userID = userID
posst.fancyPostDescription = Helper.createAttributedString(author: author, postText: postDescription)
posst.postDescription = author + ": " + postDescription
posst.timestamp = timestamp
posst.table = table
posst.region = region
posst.category = category
posst.numberOfComments = numberOfComments
posst.userWhoPostedLabel = Helper.createAttributedPostLabel(username: author, table: table, region: region, category: category)
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
for(_, person) in people {
posst.peopleWhoLike.append(person as! String)
}
}
currentTablePosts.insert(posst, at: 0)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableCollectionView"), object: nil)
} // end of if let
}
}
})
tableReference.removeAllObservers()
}
static func getMoreTablePosts(tableNumber: String, lastVisibleKey: String) {
print("FIRED...")
let currentNumberOfPosts = currentTablePosts.count
print("Number of posts before fetiching ", currentNumberOfPosts)
print("Oldest post key ", oldestTableKeys[tableNumber] ?? "not set yet", "***********")
tableReference.child(tableNumber).queryOrderedByKey().queryEnding(atValue: lastVisibleKey).queryLimited(toLast: 12).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let post = child?.value as? [String: AnyObject] {
if let id = post["postID"] as? String {
if id == lastVisibleKey {
return
}
}
let posst = Post()
if let author = post["author"] as? String, let likes = post["likes"] as? Int, let pathToImage = post["pathToImage"] as? String, let postID = post["postID"] as? String, let postDescription = post["postDescription"] as? String, let timestamp = post["timestamp"] as? Double, let category = post["category"] as? String, let table = post["group"] as? String, let userID = post["userID"] as? String, let numberOfComments = post["numberOfComments"] as? Int, let region = post["region"] as? String {
posst.author = author
posst.likes = likes
posst.pathToImage = pathToImage
posst.postID = postID
posst.userID = userID
posst.fancyPostDescription = Helper.createAttributedString(author: author, postText: postDescription)
posst.postDescription = author + ": " + postDescription
posst.timestamp = timestamp
posst.table = table
posst.region = region
posst.category = category
posst.numberOfComments = numberOfComments
posst.userWhoPostedLabel = Helper.createAttributedPostLabel(username: author, table: table, region: region, category: category)
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
for(_, person) in people {
posst.peopleWhoLike.append(person as! String)
}
}
currentTablePosts.insert(posst, at: currentNumberOfPosts)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTableCollectionView"), object: nil)
if let oldestTableKey = oldestTableKeys[tableNumber] {
if postID == oldestTableKey {
print("returning")
print("number of posts on return \(currentTablePosts.count)")
return
}
}
} // end if let
}
}
})
tableReference.removeAllObservers()
}
* UPDATE *
The image caching used here still had some issues in my experience. I have since moved on to using Kingfisher which is extremely easy to setup and use.
* OLD SOLUTION *
Found a solution based on the answer suggested in the comments.
I modified the extension I am using to cache my images. Although in the future I think I will subclass UIImageView. Here is the modified version of my code.
import UIKit
let userImageCache = NSCache<NSString, UIImage>()
let imageCache = NSCache<AnyObject, AnyObject>()
var imageURLString: String?
extension UIImageView {
public func imageFromServerURL(urlString: String, collectionView: UICollectionView, indexpath : IndexPath) {
imageURLString = urlString
if let url = URL(string: urlString) {
image = nil
if let imageFromCache = imageCache.object(forKey: urlString as AnyObject) as? UIImage {
self.image = imageFromCache
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if error != nil{
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async(execute: {
if let imgaeToCache = UIImage(data: data!){
if imageURLString == urlString {
self.image = imgaeToCache
}
imageCache.setObject(imgaeToCache, forKey: urlString as AnyObject)// calls when scrolling
collectionView.reloadItems(at: [indexpath])
}
})
}) .resume()
}
}
func downloadImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = imageCache.object(forKey: imgURL! as AnyObject) as? UIImage {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
imageCache.setObject(imageToCache!, forKey: imgURL! as AnyObject)
self.image = imageToCache
}
}
task.resume()
}
func downloadUserImage(from imgURL: String!) {
let url = URLRequest(url: URL(string: imgURL)!)
// set initial image to nil so it doesn't use the image from a reused cell
image = nil
// check if the image is already in the cache
if let imageToCache = userImageCache.object(forKey: imgURL! as NSString) {
self.image = imageToCache
return
}
// download the image asynchronously
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
// user an alert to display the error
if let topController = UIApplication.topViewController() {
Helper.showAlertMessage(vc: topController, title: "Error Downloading Image", message: error as! String)
}
return
}
DispatchQueue.main.async {
// create UIImage
let imageToCache = UIImage(data: data!)
// add image to cache
userImageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
self.image = imageToCache
}
}
task.resume()
}
}
I created the first method to cache the images for the collectionView and the other methods are still used for tableViews. The cache was also changed from let imageCache = NSCache<NSString, UIImage>() to let imageCache = NSCache<AnyObject, AnyObject>()
I have an iOS swift app using Firebase realtime database. If I use the app normally so far I cannot find any issue. However, I want to anticipate edge cases.
I am trying to stress test my app before I push the update, and one way I am doing it is quickly going back and forth from a VC with a tableView to the next VC which is a detail VC. If I do it several times eventually the tableview will show lots of duplicate data.
I have tested my app by having a tableview open on my simulator and going into my Firebase Console and manually changing a value and instantly on the device the string changes.
So I am confused as to why my tableview would show an incorrect amount of children if it is constantly checking what the value should be.
// MARK: Firebase Methods
func checkIfDataExits() {
DispatchQueue.main.async {
self.cardArray.removeAll()
self.ref.observe(DataEventType.value, with: { (snapshot) in
if snapshot.hasChild("cards") {
self.pullAllUsersCards()
} else {
self.tableView.reloadData()
}
})
}
}
func pullAllUsersCards() {
cardArray.removeAll()
let userRef = ref.child("users").child((user?.uid)!).child("cards")
userRef.observe(DataEventType.value, with: { (snapshot) in
for userscard in snapshot.children {
let cardID = (userscard as AnyObject).key as String
let cardRef = self.ref.child("cards").child(cardID)
cardRef.observe(DataEventType.value, with: { (cardSnapShot) in
let cardSnap = cardSnapShot as DataSnapshot
let cardDict = cardSnap.value as! [String: AnyObject]
let cardNickname = cardDict["nickname"]
let cardType = cardDict["type"]
let cardStatus = cardDict["cardStatus"]
self.cardNicknameToTransfer = cardNickname as! String
self.cardtypeToTransfer = cardType as! String
let aCard = CardClass()
aCard.cardID = cardID
aCard.nickname = cardNickname as! String
aCard.type = cardType as! String
aCard.cStatus = cardStatus as! Bool
self.cardArray.append(aCard)
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
})
}
I got help and changed my code drastically, so now it works
func checkIfDataExits() {
self.ref.observe(DataEventType.value, with: { (snapshot) in
if snapshot.hasChild("services") {
self.pullCardData()
} else {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
})
}
func pullCardData() {
let cardRef = self.ref.child("cards")
cardRef.observe(DataEventType.value, with: { (snapshot) in
for cards in snapshot.children {
let allCardIDs = (cards as AnyObject).key as String
if allCardIDs == self.cardID {
if let childId = self.cardID {
let thisCardLocation = cardRef.child(childId)
thisCardLocation.observe(DataEventType.value, with: { (snapshot) in
let thisCardDetails = snapshot as DataSnapshot
if let cardDict = thisCardDetails.value as? [String: AnyObject] {
self.selectedCard?.cardID = thisCardDetails.key
self.selectedCard?.nickname = cardDict["nickname"] as? String ?? ""
self.selectedCard?.type = cardDict["type"] as? String ?? ""
self.pullServicesForCard()
}
})
}
}
}
})
}
func pullServicesForCard() {
if let theId = self.cardID {
let thisCardServices = self.ref.child("cards").child(theId).child("services")
thisCardServices.observe(DataEventType.value, with: { (serviceSnap) in
if self.serviceArray.count != Int(serviceSnap.childrenCount) {
self.serviceArray.removeAll()
self.fetchAndAddAllServices(serviceSnap: serviceSnap, index: 0, completion: { (success) in
if success {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
})
}
})
}
}
func fetchAndAddAllServices(serviceSnap: DataSnapshot, index: Int, completion: #escaping (_ success: Bool) -> Void) {
if serviceSnap.hasChildren() {
if index < serviceSnap.children.allObjects.count {
let serviceChild = serviceSnap.children.allObjects[index]
let serviceID = (serviceChild as AnyObject).key as String
let thisServiceLocationInServiceNode = self.ref.child("services").child(serviceID)
thisServiceLocationInServiceNode.observeSingleEvent(of: DataEventType.value, with: { (thisSnap) in
let serv = thisSnap as DataSnapshot
if let serviceDict = serv.value as? [String: AnyObject] {
let aService = ServiceClass(serviceDict: serviceDict)
self.serviceCurrent = serviceDict["serviceStatus"] as? Bool
self.serviceName = serviceDict["serviceName"] as? String ?? ""
self.serviceURL = serviceDict["serviceURL"] as? String ?? ""
self.serviceFixedBool = serviceDict["serviceFixed"] as? Bool
self.serviceFixedAmount = serviceDict["serviceAmount"] as? String ?? ""
self.attentionInt = serviceDict["attentionInt"] as? Int
self.totalArr.append((serviceDict["serviceAmount"] as? String)!)
// self.doubleArray = self.totalArr.flatMap{ Double($0) }
// let arraySum = self.doubleArray.reduce(0, +)
// self.title = self.selectedCard?.nickname ?? ""
// if let titleName = self.selectedCard?.nickname {
// self.title = "\(titleName): \(arraySum)"
// }
aService.serviceID = serviceID
if serviceDict["serviceStatus"] as? Bool == true {
self.selectedCard?.cStatus = true
} else {
self.selectedCard?.cStatus = false
}
if !self.serviceArray.contains(where: { (service) -> Bool in
return service.serviceID == aService.serviceID
}) {
self.serviceArray.append(aService)
self.serviceArray.sort {$1.serviceAttention < $0.serviceAttention}
}
}
self.fetchAndAddAllServices(serviceSnap: serviceSnap, index: index + 1, completion: completion)
})
}
else {
completion(true)
}
}
else {
completion(false)
}
}
I'm trying to create a chat applications with tableview to show the messages. Everything works fine except that some cells just won't show. It aren't always the same cells. I'm getting the messages from my Firebase database.
My Code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = cellCache.object(forKey: indexPath as AnyObject) as? UITableViewCell{
return cell
}
let cell = messagesTableView.dequeueReusableCell(withIdentifier: "otherMessageCell") as! OtherMessageTableViewCell
DispatchQueue.global(qos: .userInteractive).async {
let message = self.messages[indexPath.row]
let ref = FIRDatabase.database().reference()
let uid = FIRAuth.auth()?.currentUser?.uid
if(uid == message.sender) {
cell.sender.textAlignment = .right
cell.message.textAlignment = .right
}else{
cell.sender.textAlignment = .left
cell.message.textAlignment = .left
}
let uidReference = ref.child("Users").child(message.sender!)
uidReference.observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let username = dictionary["Username"] as! String
let imageLink = dictionary["Image Link"] as! String
cell.sender.text = username
cell.message.text = message.message
cell.profileImage.image = nil
cell.profileImage.loadImageWithURLString(urlString: imageLink)
}
}, withCancel: nil)
}
DispatchQueue.main.async {
self.cellCache.setObject(cell, forKey: indexPath as AnyObject)
}
return cell
}
Example:
I hope someone will be able to help me. Thanks
I've found a solution:
var savedSenders = [Int: String]()
var savedMessages = [Int: String]()
var savedImages = [Int: UIImage]()
var savedAlignments = [Int: NSTextAlignment]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = messagesTableView.dequeueReusableCell(withIdentifier: "otherMessageCell") as! OtherMessageTableViewCell
if(savedSenders[indexPath.row] != nil && savedMessages[indexPath.row] != nil && savedImages[indexPath.row] != nil && savedAlignments[indexPath.row] != nil) {
cell.sender.textAlignment = savedAlignments[indexPath.row]!
cell.message.textAlignment = savedAlignments[indexPath.row]!
cell.sender.text = savedSenders[indexPath.row]
cell.message.text = savedMessages[indexPath.row]
cell.profileImage.image = savedImages[indexPath.row]
return cell
}
cell.sender.text = ""
cell.message.text = ""
cell.profileImage.image = nil
DispatchQueue.global(qos: .userInteractive).async {
let message = self.messages[indexPath.row]
let ref = FIRDatabase.database().reference()
let uid = FIRAuth.auth()?.currentUser?.uid
if(uid == message.sender) {
cell.sender.textAlignment = .right
cell.message.textAlignment = .right
self.savedAlignments.updateValue(.right, forKey: indexPath.row)
}else{
cell.sender.textAlignment = .left
cell.message.textAlignment = .left
self.savedAlignments.updateValue(.left, forKey: indexPath.row)
}
let uidReference = ref.child("Users").child(message.sender!)
uidReference.observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let username = dictionary["Username"] as! String
let imageLink = dictionary["Image Link"] as! String
cell.sender.text = username
cell.message.text = message.message
cell.profileImage.image = nil
cell.profileImage.loadImageWithURLString(urlString: imageLink)
let url = URL(string: imageLink)
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if(error != nil){
print(error as Any)
return
}
DispatchQueue.main.async {
if let downloadedImage = UIImage(data: data!) {
let image = downloadedImage
self.savedSenders.updateValue(username, forKey: indexPath.row)
self.savedMessages.updateValue(message.message!, forKey: indexPath.row)
self.savedImages.updateValue(image, forKey: indexPath.row)
}
}
}.resume()
}
}, withCancel: nil)
}
return cell
}
The data for every cell is saved into the arrays (savedSenders, savedMessages, savedImages and savedAlignments). If I don't save it into arrays, the cells will have to load all the data from Firebase and this will take longer. If it takes longer, it won't look good.
I tested everything and it works.