Return Value in Completion of Function - ios

I have built my own swift framework for running queries on a MySQL database. I am currently developing a chat application in which I need to display an avatar image for each user who is currently chatting.
I am retrieving the user's profile picture URL from my database and then I need to return a UIImage once I get the profile picture URL. I get the error Unexpected non-void return value in void function. Is there a way to make this easier or bypass this error?
This is my full code:
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
let currentMessage = self.messages[indexPath.row]
if (currentMessage.senderId == self.senderId) {
// Current User sent message
var imageURL: NSURL!
ConnectionManager.sharedInstance.complexQuery("SELECT profile_picture FROM users WHERE username='\(self.senderId)' ", completion: { (result) in
if let results = result as? [[String: AnyObject]] {
for result in results {
if let profilePictureURL = result["profile_picture"] {
print(profilePictureURL)
imageURL = profilePictureURL as! NSURL
let imageData = NSData(contentsOfURL: imageURL)
let image = UIImage(data: imageData!)
// ERROR HERE (line below)
return JSQMessagesAvatarImageFactory.avatarImageWithImage(image, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
}
}
}
})
} else {
var imageURL: NSURL!
ConnectionManager.sharedInstance.complexQuery("SELECT profile_picture FROM users WHERE username='\(currentMessage.senderId)' ", completion: { (result) in
if let results = result as? [[String: AnyObject]] {
for result in results {
if let profilePictureURL = result["profile_picture"] {
print(profilePictureURL)
imageURL = profilePictureURL as! NSURL
let imageData = NSData(contentsOfURL: imageURL)
let image = UIImage(data: imageData!)
// ERROR HERE (line below)
return JSQMessagesAvatarImageFactory.avatarImageWithImage(image, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
}
}
}
})
}
}

It seems that ConnectionManager.sharedInstance.complexQuery is an asynchronous function, so the main thread will continue after calling it, there will be nothing left in this context to receive what will be returned
so, do what ever you want with the image in the callback function "completion"
for example, you may get a reference of your UIImageView and set it's image when you get it, but you need to use another data source delegate method
collectionView(_:cellForItemAtIndexPath:)
Something like this:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(YOUR_ID,
forIndexPath: indexPath) as! YourOwnCell
ConnectionManager.sharedInstance.complexQuery("SELECT profile_picture FROM users WHERE username='\(currentMessage.senderId)' ", completion: { (result) in
if let results = result as? [[String: AnyObject]] {
for result in results {
if let profilePictureURL = result["profile_picture"] {
print(profilePictureURL)
imageURL = profilePictureURL as! NSURL
let imageData = NSData(contentsOfURL: imageURL)
let image = UIImage(data: imageData!)
// Assuming avatarImageWithImage is returning UIImageView
cell.imageView = JSQMessagesAvatarImageFactory.avatarImageWithImage(image, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
}
}
}
})
return cell;
}

What you try isn't how asynchronous functions work. It's not how they could work. The whole point of an asynchronous call is that your app can continue running while the asynchronous call continues running in the background. Your complexQuery function returns immediately so that you can continue.
Instead of this function returning the value that you want, your completion code must deposit the value in the right place.

Related

TableView with labels, images, gifs and video hangs / gets stuck incorrect while fetch from firestore in iOS, Swift

I have tableview with label, imageView (for image, gif & video thumbnail). I am sure that doing something wrong and I can't handle its completion handler due to which the app is hanged and gets stuck for a long time.
My model is like,
struct PostiisCollection {
var id :String?
var userID: String?
var leadDetails : NSDictionary?
var company: NSDictionary?
var content: String?
init(Doc: DocumentSnapshot) {
self.id = Doc.documentID
self.userID = Doc.get("userID") as? String ?? ""
self.leadDetails = Doc.get("postiiDetails") as? NSDictionary
self.company = Doc.get("company") as? NSDictionary
self.content = Doc.get("content") as? String ?? ""
}
}
I wrote in my view controller for fetch this,
var postiisCollectionDetails = [PostiisCollection]()
override func viewDidLoad() {
super.viewDidLoad()
let docRef = Firestore.firestore().collection("PostiisCollection").whereField("accessType", isEqualTo: "all_access")
docRef.getDocuments { (querysnapshot, error) in
if let doc = querysnapshot?.documents, !doc.isEmpty {
print("Document is present.")
for document in querysnapshot!.documents {
_ = document.documentID
if let compCode = document.get("company") as? NSDictionary {
do {
let jsonData = try JSONSerialization.data(withJSONObject: compCode)
let companyPost: Company = try! JSONDecoder().decode(Company.self, from: jsonData)
if companyPost.companyCode == AuthService.instance.companyId ?? ""{
print(AuthService.instance.companyId ?? "")
if (document.get("postiiDetails") as? NSDictionary) != nil {
let commentItem = PostiisCollection(Doc: document)
self.postiisCollectionDetails.append(commentItem)
}
}
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async {
self.tableView.isHidden = false
self.tableView.reloadData()
}
}
}
}
}
}
I need to check for the index path with image view is either image or gif or video with different parameters, I tried with tableview delegate and datasource method by,
extension AllAccessPostiiVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postiisCollectionDetails.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AllAccessCell", for: indexPath)
let label1 = cell.viewWithTag(1) as? UILabel
let imagePointer = cell.viewWithTag(3) as? UIImageView
let getGif = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "video") as? NSArray
label1?.text = "\(arrPostiisCollectionFilter[indexPath.row].leadDetails?.value(forKey: "title"))"
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
print(arrGif[0])
let gifURL : String = "\(arrGif[0])"
let imageURL = UIImage.gifImageWithURL(gifURL)
imagePointer?.image = imageURL
playButton?.isHidden = true
}
if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
print(arrPhoto[0])
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
do {
let data = try Data(contentsOf: url!)
let image = UIImage(data: data as Data)
DispatchQueue.main.async {
imagePointer?.image = image
playButton?.isHidden = true
}
} catch {
print(error)
}
})
}
if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL = URL(string: arrVideo[0])
let asset = AVAsset(url:videoURL!)
if let videoThumbnail = asset.videoThumbnail{
SVProgressHUD.dismiss()
imagePointer?.image = videoThumbnail
playButton?.isHidden = false
}
}
}
}
If I run, the app hangs in this page and data load time is getting more, some cases the preview image is wrongly displayed and not able to handle its completion
As others have mentioned in the comments, you are better of not performing the background loading in cellFroRowAtIndexPath.
Instead, it's better practice to add a new method fetchData(), where you perform all the server interaction.
So for example:
// Add instance variables for fast access to data
private var images = [UIImage]()
private var thumbnails = [UIImage]()
private func fetchData(completion: ()->()) {
// Load storage URLs
var storageURLs = ...
// Load data from firebase
let storageRef = Storage.storage().reference(forURL: arrPhoto[0])
storageRef.downloadURL(completion: { (url, error) in
// Parse data and store resulting image in image array
...
// Call completion handler to indicate that loading has finished
completion()
})
}
Now you can call fetchData() whenever you would like to refresh data and call tableview.reloadData() within the completion handler. That of course must be done on the main thread.
This approach simplifies your cellForRowAtIndexPath method.
There you can just say:
imagePointer?.image = ...Correct image from image array...
Without any background loading.
I suggest using below lightweight extension for image downloading from URL
using NSCache
extension UIImageView {
func downloadImage(urlString: String, success: ((_ image: UIImage?) -> Void)? = nil, failure: ((String) -> Void)? = nil) {
let imageCache = NSCache<NSString, UIImage>()
DispatchQueue.main.async {[weak self] in
self?.image = nil
}
if let image = imageCache.object(forKey: urlString as NSString) {
DispatchQueue.main.async {[weak self] in
self?.image = image
}
success?(image)
} else {
guard let url = URL(string: urlString) else {
print("failed to create url")
return
}
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
// response received, now switch back to main queue
DispatchQueue.main.async {[weak self] in
if let error = error {
failure?(error.localizedDescription)
}
else if let data = data, let image = UIImage(data: data) {
imageCache.setObject(image, forKey: url.absoluteString as NSString)
self?.image = image
success?(image)
} else {
failure?("Image not available")
}
}
}
task.resume()
}
}
}
Usage:
let path = "https://i.stack.imgur.com/o5YNI.jpg"
let imageView = UIImageView() // your imageView, which will download image
imageView.downloadImage(urlString: path)
No need to put imageView.downloadImage(urlString: path) in mainQueue, its handled in extension
In your case:
You can implement following logic in cellForRowAt method
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let urlString : String = "\(arrGif[0])"
let image = UIImage.gifImageWithURL(urlString)
imagePointer?.image = image
playButton?.isHidden = true
}
else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let urlString = Storage.storage().reference(forURL: arrPhoto[0])
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = true
}
elseif getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let urlString = arrVideo[0]
imagePointer?.downloadImage(urlString: urlString)
playButton?.isHidden = false
}
If you have one imageView to reload in tableView for photo, video and gif. then use one image array to store it prior before reloading. So that your main issue of hang or stuck will be resolved. Here the main issue is each time in table view cell collection data is being called and checked while scrolling.
Now I suggest to get all photo, gifs and video (thumbnail) as one single array prior to table view reload try this,
var cacheImages = [UIImage]()
private func fetchData(completionBlock: () -> ()) {
for (index, _) in postiisCollectionDetails.enumerated() {
let getGif = postiisCollectionDetails[index].leadDetails?.value(forKey: "gif") as? NSArray
let getPhoto = postiisCollectionDetails[index].leadDetails?.value(forKey: "photo") as? NSArray
let getVideo = postiisCollectionDetails[index].leadDetails?.value(forKey: "video") as? NSArray
if getGif != nil {
let arrGif = getGif?.value(forKey: "gifUrl") as! [String]
let gifURL : String = "\(arrGif[0])"
self.randomList.append(gifURL)
/////---------------------------
let imageURL = UIImage.gifImageWithURL(gifURL)
self.cacheImages.append(imageURL!)
//////=================
}
else if getVideo != nil {
let arrVideo = getVideo?.value(forKey: "videoUrl") as! [String]
let videoURL: String = "\(arrVideo[0])"
let videoUrl = URL(string: arrVideo[0])
let asset = AVAsset(url:videoUrl!)
if let videoThumbnail = asset.videoThumbnail{
////--------------
self.cacheImages.append(videoThumbnail)
//-----------
}
self.randomList.append(videoURL)
}else if getPhoto != nil {
let arrPhoto = getPhoto?.value(forKey: "photoUrl") as! [String]
let photoURL : String = "\(arrPhoto[0])"
/////---------------------------
let url = URL(string: photoURL)
let data = try? Data(contentsOf: url!)
if let imageData = data {
let image = UIImage(data: imageData)
if image != nil {
self.cacheImages.append(image!)
}
else {
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
}
}
//////=================
}
else {
//-----------------
let defaultImage: UIImage = UIImage(named:"edit-user-80")!
self.cacheImages.append(defaultImage)
//--------------------
}
}
completionBlock()
}
After getting all UIImage as array where loop is being called. Now you call this function inside your viewDidLoad. So after all values in images fetched then try to call tableView like this,
override func viewDidLoad() {
self.fetchData {
DispatchQueue.main.async
self.tableView.reloadData()
}
}
}
Now atlast, you may use SDWebImage or any other background image class or download method to call those in tableView cellforRow method,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// your cell idetifier & other stuffs
if getVideo != nil {
imagePointer?.image = cacheImages[indexPath.row]
playButton?.isHidden = false
}else {
imagePointer?.image = cacheImages[indexPath.row]
// or get photo with string via SdWebImage
// imagePointer?.sd_setImage(with: URL(string: photoURL), placeholderImage: UIImage(named: "edit-user-80"))
playButton?.isHidden = true
}
return cell
}
You're handling data in a totally wrong manner. Data(contentsOf: url!) - This is wrong. You should chache the images and should download it to directory. When you convert something into data it takes place into the memory(ram) and it is not good idea when playing with large files. You should use SDWebImage kind of library to set images to imageview.
Second thing if let videoThumbnail = asset.videoThumbnail - This is also wrong. Why you're creating assets and then getting thumbnail from it? You should have separate URL for the thumbnail image for your all videos in the response of the API and then again you can use SDWebImage to load that thumbnail.
You can use SDWebImage for gif as well.
Alternative of SDWebImage is Kingfisher. Just go through both libraries and use whatever suitable for you.

Firebase Storage async image download random order

I'm trying to download images from firebase storage to display in my collectionview cells, but the images keep appearing in random order in the cells. The cells each have a label that is retrieved from firebase storage (item1, item2 etc) which displays nicely in the correct cell every time. The images stored in firebase storage each have their storage url as a child to their respective item name in the firebase database.
I'm sucesfully able to retrieve each image url, and download all the images and display them in the cells correctly, it's just that they keep appearing in randomized order every time I open the app, so the image does not correspond with the item name label.
I realize i need to asyncronously download the images, so each image finishes loading in the correct cell before continuing to the next, but I'm having trouble doing so. Heres my code so far:
func downloadImg(completion: #escaping (UIImage?) -> ()) {
let ref = Database.database().reference().child("somePath")
ref.observeSingleEvent(of: .value) { (snapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storageUrl")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
DispatchQueue.main.async {
completion(image)
}
}
}
}
}
}
}
Then I call my function in the viewdidload:
downloadImg { (completion) in
self.itemPicArray.append(completion!)
self.collectionView?.reloadData()
}
Finally i set my cell imageview to itemPicArray[indexPath.row]
Like I said, this works perfectly except the fact that the images keep showing up randomly. Help very much appreciated!
Your problem is that everytime an image comes in, you reload the entire collection view. Depending on the sizes of the images and the state of the network, the images will come in in a different order almost every time.
Consider downloading all of the images first and then reloading the collection view once. If there are a lot of images, consider paginating your results. You can enumerate the loop and sort the data source array by this original order. I've added a custom data object to help with that.
class CustomObject {
var image: UIImage?
let n: Int
init(image: UIImage?, n: Int) {
self.image = image
self.n = n
}
}
let dispatch = DispatchGroup()
for (n, item) in snapshot.children.enumerated() {
let object = CustomObject(image: nil, n: n) // init custom object with n (image is still nil)
dispatch.enter() // enter dispatch
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in // download image
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
object.image = image // inject custom object with image
itemPicArray.append(object) // append to array
}
dispatch.leave() // leave dispatch
}
}
dispatch.notify(queue: .global()) { // dispatch completion
itemPicArray.sort { $0.n < $1.n } // sort by n (original download order)
collectionView.reloadData() // reload collection view
}
Using a model could be a good idea.
struct Image {
var imageName: String
var image: UIImage
}
This way, no matter the order, the item name (image name) and the image will be paired.
Perhaps a better solution now is to configure method downloadImg so that it takes the imageName as a parameter. Then you can call the correct node to get the corresponding storageURL.
func downloadImg(imageName: String, completion: #escaping (Image?) -> ()) {
// Use the parameter to create your database reference
let ref = Database.database().reference().child(imageName)
ref.observeSingleEvent(of: .value) { (snapshot) in
for item in snapshot.children {
let snap = item as! DataSnapshot
let imageSnap = snap.childSnapshot(forPath: "img/storageUrl")
if let url = imageSnap.value as? String {
let someRef = self.storageRef.reference(forURL: url)
someRef.getData(maxSize: 10 * 10024 * 10024) { data, error in
if let error = error {
print(error)
return
}
if let image = UIImage(data: data) {
// Create a variable of type Image (your custom model)
let imageWithName = Image(imageName: imageName, image: image)
completion(imageWithName)
}
}
}
}
}
}
Calling and handling could be done like so:
// Create a variable to hold your item name/image-pairs
var imagesWithNames = [Image]()
let dispatchGroup = DispatchGroup()
// Iterate over your array of item names
for item in itemArray {
dispatchGroup.enter()
downloadImg(item) { (imageWithName) in
self.imagesWithNames.append(imageWithName)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) { {
self.collectionView?.reloadData()
}
And to populate the collectionView you can go:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! YourCustomCell
// Get the pair at the given index
let imageWithName = self.imagesWithNames[indexPath.row]
DispatchQueue.main.async {
// Set image and item label (example below)
self.yourImageView.image = imageWithName.image
self.yourItemLabel.text = imageWithName.imageName
}
return cell
}
If anyone is having issues with this in the future, use -bsod answer and create a custom object. Also create a variable var counter: Int = 0. Here's how my code looks like Swift 5.X and it works perfectly.
class CustomObject {
var image: UIImage?
let n: Int
init(image: UIImage?, n: Int) {
self.image = image
self.n = n
}
}
func reloadStuff() {
dispatch.notify(queue: .main) {
self.imageArrayCells.sort { $0.n < $1.n }
self.contentViewProfile.collectionView.refreshControl?.endRefreshing()
self.contentViewProfile.collectionView.reloadData()
}
}
for i in 0 ..< self.imageArrayInfo.count {
let object = CustomObject(image: nil, n: i)
let urlString = self.imageArrayInfo[i]
print(object.n)
let url = URL(string: urlString)
self.dispatch.enter()
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
self.dispatch.enter()
guard let data = data, error == nil else {
return
}
guard let image = UIImage(data: data) else {
return
}
object.image = image
self.imageArrayCells.append(object)
self.dispatch.leave()
self.counter += 1
if self.counter == self.imageArrayInfo.count {
for i in 0 ..< self.imageArrayInfo.count {
self.dispatch.leave()
}
self.counter = 0
}
}
task.resume()
}
self.reloadStuff()
Here's what I'm calling in collectionView(cellForItemAt:_)
cell.imageInProfileCollection.image = imageArrayCells[indexPath.row].image

Freezes during scroll in UITableView during parsing JSON data

I'm struggling with multithreading in news app. The thing is - my application freezes often when I scroll table view after data was parsed and loaded and its way too often. I think I'm some kind of wrong of reloading data every time.
First part:
final let urlString = "http://api.to.parse"
Here I create array of structs to fill in my data
struct jsonObjects {
var id : Int
var date : String
var title : String
var imageURL : URL
}
var jsonData = [jsonObjects]()
Here's my viewDidLoad of tableView
override func viewDidLoad() {
super.viewDidLoad()
// MARK : - Download JSON info on start
JsonManager.downloadJsonWithURL(urlString: urlString, сompletion: {(jsonArray) -> Void in
guard let data = jsonArray else { print("Empty dude"); return;}
for jsonObject in data {
if let objectsDict = jsonObject as? NSDictionary {
guard
let id = objectsDict.value(forKey: "id") as? Int,
let date = objectsDict.value(forKey: "date") as? String,
let titleUnparsed = objectsDict.value(forKey: "title") as? NSDictionary,
let title = (titleUnparsed as NSDictionary).value(forKey: "rendered") as? String,
let imageString = objectsDict.value(forKey: "featured_image_url") as? String,
let imageURL = NSURL(string: imageString) as URL?
else {
print("Error connecting to server")
return
}
There I go with appending filled structure to array:
self.jsonData.append(jsonObjects(id: id, date: date, title: title,
imageURL: imageURL))
}
}
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
})
and downloadJsonWithURL is simply:
class JsonManager {
class func downloadJsonWithURL(urlString: String, сompletion: #escaping (NSArray?) -> Void) {
guard let url = NSURL(string: urlString) else { print("There is no connection to the internet"); return;}
URLSession.shared.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
guard let parseData = data else { print("There is no data"); return;}
if let jsonObj = try? JSONSerialization.jsonObject(with: parseData, options: .allowFragments)
as? NSArray {
сompletion(jsonObj)
}
}).resume()
}
And finally - I input that in my TableViewCell:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return jsonData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "newscell") as? NewsTableViewCell else {
fatalError("Could not find cell by identifier")
}
guard let imageData = NSData(contentsOf: jsonData[indexPath.row].imageURL) else {
fatalError("Could not find image")
}
cell.newsTitleLabel.text = self.jsonData[indexPath.row].title
cell.newsTitleLabel.font = UIFont.boldSystemFont(ofSize: 20.0)
cell.newsImageView.image = UIImage(data: imageData as Data)
return cell
}
So there are two questions: how should I distribute my threads and how should I call them so that I have smooth and nice tableview with all downloaded data? and how should I reload data in cell?
Your issue is caused by the imageData its blocking the main thread. The best way to solve this is to download all the images into an image cache. And I would most certainly remove the downloading of images from within the cellForRowAtIndexPath.
Downloading data, parsing in background thread, the updating the UI on main-thread.
Basically if you do correctly like this, everything will be okay.
So you may need to double check one more time if you are rendering UI on main-thread.
On the debugging panel, there's pause/play button.
So whenever your app frozen, try to pause the app immediately:
1) Then check if any of your UI method is running on background-thread.
2) Check if your downloading task or parsing json doing on main-thread.
If it falls under above cases, it needs to be correct.

Why is my function to download avatars being called thousands of times?

My print statement shows that the function is called 4771 times in about 15 seconds, obviously resulting in a crash.
This is the function:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
count += 1
print("\n\nAvatar func called \(count)\n")
let databaseRef = FIRDatabase.database().reference()
let message = messages[indexPath.item]
let placeHolderImage = UIImage(named: "Logo")
let avatarImage = JSQMessagesAvatarImage(avatarImage: nil, highlightedImage: nil, placeholderImage: placeHolderImage)
if let messageID = message.senderId {
// Check cache for avatar
if imageCache.object(forKey: messageID as NSString) != nil {
DispatchQueue.main.async {
avatarImage!.avatarImage = imageCache.object(forKey: messageID as NSString)
avatarImage!.avatarHighlightedImage = imageCache.object(forKey: messageID as NSString)
self.collectionView.reloadData()
}
} else {
// If avatar isn't cached, fire off a new download
databaseRef.child("users").child(messageID).observe(.value, with: { (snapshot) in
if let profilePic = (snapshot.value as AnyObject!)!["profilePicture"] as! String! {
let profilePicURL: URL = URL(string: profilePic)!
Alamofire.request(profilePicURL)
.responseImage { response in
if let downloadedImage = response.result.value {
imageCache.setObject(downloadedImage, forKey: message.senderId as NSString)
DispatchQueue.main.async {
avatarImage!.avatarImage = imageCache.object(forKey: message.senderId as NSString)
avatarImage!.avatarHighlightedImage = imageCache.object(forKey: message.senderId as NSString)
self.collectionView.reloadData()
}
}
}
}
})
}
}
return avatarImage
}
What's causing the loop? There's only one user (me) to get an avatar for anyway. I'm somewhat new to programming and am trying to figure out how to work with a cache... my intention with this function is to check if the user's avatar is cached, and if so, use it. If not, fire off a new download from Firebase. But I am messing up badly apparently - How can I write this so it efficiently checks the cache and/or downloads the image, and doesn't get stuck in a loop?
You are calling reloadData in your function, which will cause this function to be called again, which calls reloadData and so on; you have created an infinite loop.
You only need to reload anything in the case where you initially return a placeholder and then subsequently retrieve the avatar from the network. In this case it is very wasteful to reload the whole collection view; you simply need to reload the affected item:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
count += 1
print("\n\nAvatar func called \(count)\n")
let databaseRef = FIRDatabase.database().reference()
let message = messages[indexPath.item]
let placeHolderImage = UIImage(named: "Logo")
let avatarImage = JSQMessagesAvatarImage(avatarImage: nil, highlightedImage: nil, placeholderImage: placeHolderImage)
if let messageID = message.senderId {
// Check cache for avatar
if let cacheObject = imageCache.object(forKey: messageID as NSString) {
avatarImage!.avatarImage = cacheObject
avatarImage!.avatarHighlightedImage = cacheObject
} else {
// If avatar isn't cached, fire off a new download
databaseRef.child("users").child(messageID).observe(.value, with: { (snapshot) in
if let profilePic = (snapshot.value as AnyObject!)!["profilePicture"] as! String! {
let profilePicURL: URL = URL(string: profilePic)!
Alamofire.request(profilePicURL)
.responseImage { response in
if let downloadedImage = response.result.value {
imageCache.setObject(downloadedImage, forKey: message.senderId as NSString)
DispatchQueue.main.async {
self.collectionView.reloadItems(at:[indexPath])
}
}
}
}
})
}
}
return avatarImage
}

Loading images using collection view and data is not displaying properly until refreshed

I'm creating an instagram clone and I'm having a small issue. When the profile page loads, i use a collection view to display all of the images of the posts the user has made, and sort these images by date.
When initially loading, what often happens is that the images will appear out of order. However when i refresh the page, the images display correctly. So when loading instead of getting images (4,3,2,1) I'll get something like (1, 3, 2, 1) then refresh and get the correct order.
When i don't use the sort command, the images always display in the correct order, but i need the newest ones to show up first.
Below is the function i use to observe my post data from Firebase, and append them to the posts array. I use an image cache to load my data.
let posts = [Post]()
func observePosts() {
let currentUser = FIRAuth.auth()?.currentUser?.uid
let ref = FIRDatabase.database().reference().child("user-posts").child(currentUser!)
ref.observe(.childAdded, with: { (snapshot) in
let postId = snapshot.key
let postRef = FIRDatabase.database().reference().child("posts").child(postId)
postRef.observe(.value, with: { (snapshot) in
if let postDict = snapshot.value as? Dictionary<String, AnyObject> {
let post = Post(postKey: postId, postData: postDict)
self.posts.append(post)
self.posts.sort(by: { (post1, post2) -> Bool in
return post1.createdAt! > post2.createdAt!
})
}
self.collectionView?.reloadData()
})
})
}
Loading images using cache:
let imageCache: NSCache<NSString, UIImage> = NSCache()
extension UIImageView {
func loadImagesUsingCacheWith(urlString: String) {
self.image = nil
//check cache for image first
if let cachedImage = imageCache.object(forKey: urlString as NSString) {
self.image = cachedImage
return
} else {
let ref = FIRStorage.storage().reference(forURL: urlString)
ref.data(withMaxSize: 2 * 1024 * 1024, completion: { (data, error) in
if error != nil {
//Handle error
} else {
if let img = UIImage(data: data!) {
self.image = img
imageCache.setObject(img, forKey: urlString as NSString)
}
}
})
}
}
}
cellForItemAt Function:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeCell", for: indexPath) as! HomeCell
let post = posts[indexPath.item]
cell.configureCell(post: post)
return cell
}
Configure Cell function
func configureCell(post: Post) {
if let imgUrl = post.imgUrl {
cellImage.loadImagesUsingCacheWith(urlString: imgUrl)
}
}
UPDATE:
Something super weird happens. In the "cellForItemAt" function, i printed some data from the posts after i declare "let post = posts[indexPath.item]" and the info from the first post always prints to the console twice, while the rest of the data from the posts only prints once. Oddly enough, the picture that is always duplicated when loading the posts is always from the first picture. Any idea as to why this is happening?

Resources