I have a function for avatars that's called in viewDidLoad (this is the function if it helps although I think the problem is not the function itself: https://pastebin.com/ksHmucTX).
In viewDidLoad I call it:
createAvatar(senderId: self.senderId, senderDisplayName: self.senderDisplayName, user: self.currentUser!, color: UIColor.lightGray)
But since I am only passing in my information (as the current user), when using the app I only see my avatar and not the avatars of everyone else in the chat.
In the avatarImageDataForItemAt function, there is an index path to figure out which message is which:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.row]
return avatars[message.senderId]
}
So I'm wondering how to get the proper senderId (i.e. the ID of whoever sent a message) in viewDidLoad so I can plug it into the createAvatars call, as opposed to what I have now, where senderId is ONLY my ID - thus making everyone's avatar appear, and not only mine (or whoever is the current user).
Thanks for any help!
EDIT:
func getParticipantInfo() {
let groupRef = databaseRef.child("groups").child(currentRoomIdGlobal)
groupRef.observe(.childAdded, with: { snapshot in
if let snapDict = snapshot.value as? [String : AnyObject] {
for each in snapDict {
let uid = each.key
let avatar = each.value["profilePicture"] as! String
let gender = each.value["gender"] as! String
let handle = each.value["handle"] as! String
let name = each.value["name"] as! String
let status = each.value["status"] as! String
// Set those to the dictionaries [UID : value]
self.avatarDictionary.setValue(avatar, forKey: uid)
self.nameDictionary.setValue(name, forKey: uid)
self.genderDictionary.setValue(gender, forKey: uid)
self.handleDictionary.setValue(handle, forKey: uid)
self.statusDictionary.setValue(status, forKey: uid)
DispatchQueue.main.async {
self.createAvatar(senderId: uid, senderDisplayName: name, photoUrl: avatar, color: UIColor.lightGray)
self.navCollectionView?.collectionView?.reloadData()
}
}
}
})
}
createAvatar:
func createAvatar(senderId: String, senderDisplayName: String, photoUrl: String, color: UIColor) {
if self.avatars[senderId] == nil {
let img = MyImageCache.sharedCache.object(forKey: senderId as AnyObject) as? UIImage
if img != nil {
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImage(with: img, diameter: 30)
} else if photoUrl != "" {
// the images are very small, so the following methods work just fine, no need for Alamofire here
if photoUrl.contains("https://firebasestorage.googleapis.com") {
self.storage.reference(forURL: photoUrl).data(withMaxSize: 1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
assertionFailure("Error with Firebase Storage")
}
else {
let newImage = UIImage(data: data!)
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImage(with: newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage!, forKey: senderId as AnyObject, cost: data!.count)
}
}
} else if let data = NSData(contentsOf: NSURL(string:photoUrl)! as URL) {
let newImage = UIImage(data: data as Data)!
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImage(with: newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage, forKey: senderId as AnyObject, cost: data.length)
} else {
// Initials
let senderName = nameDictionary.value(forKey: senderId) as! String
let initials = senderName.components(separatedBy: " ").reduce("") { ($0 == "" ? "" : "\($0.characters.first!)") + "\($1.characters.first!)" }
let placeholder = JSQMessagesAvatarImageFactory.avatarImage(withUserInitials: initials, backgroundColor: UIColor.gray, textColor: UIColor.white, font: UIFont.systemFont(ofSize: 14), diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
MyImageCache.sharedCache.setObject(placeholder!, forKey: senderId as AnyObject)
}
}
} else {
// Initials
let senderName = nameDictionary.value(forKey: senderId) as! String
let initials = senderName.components(separatedBy: " ").reduce("") { ($0 == "" ? "" : "\($0.characters.first!)") + "\($1.characters.first!)" }
let placeholder = JSQMessagesAvatarImageFactory.avatarImage(withUserInitials: initials, backgroundColor: UIColor.gray, textColor: UIColor.white, font: UIFont.systemFont(ofSize: 14), diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
MyImageCache.sharedCache.setObject(placeholder!, forKey: senderId as AnyObject)
}
}
As far as I know, the JSQMessage contains the senderId of the sender, so the function below already specifies what avatar to be returned:
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.row]
return avatars[message.senderId]
}
You will need to add an avatar to
var avatars = [String: JSQMessagesAvatarImage]()
I assume you have the userId of that user, so get the avatar the same way every time someone joins the conversation. Lets say userToChatWith:
createAvatar(userToChatWith.userKey, senderDisplayName: userToChatWithName, user: userToChatWith, color: UIColor.lightGray)
Related
I am trying to push data from a tableView to a View controller. I can successfully transfer some of the data over, but I am still missing some key points. I will try to illustrate my question to the best of my abilities. In my notificationTableView, I have data that is stored such as a userName, userImage, jobName and jobImage. I can succesfully push over the users image and name, however The jobName and JobImage fails to be transferred over as we can see in the Images below.
In this image, we can see the tableView sections that have the userName, userImage, jobName and jobImage.
In the second image, we can see that the usersName, and Image is succesfully pushed. However, the jobImage and name are not transferred.
the code that I use to push over the information is
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let notification = notifications[indexPath.row]
if notification.notificationType != .swipe {
let acceptWorker = jobProgressViewController()
acceptWorker.workerUser = myUser
acceptWorker.workerUser = notification.user
jobProgressView?.myParentViewController = self
let navController = UINavigationController(rootViewController: acceptWorker)
present(navController, animated: true, completion: nil)
} else {
print("something else should go here")
}
and the code that I use to retrieve the information is below. which is my jobProgressViewController
var notification: userNotifications?
var workerUser: User? {
didSet {
let name = workerUser?.name
workerNameLabel.text = name
guard let profileImage = workerUser?.profileImageUrl else { return }
workerImageView.loadImageUsingCacheWithUrlString(profileImage)
if let post = notification?.poster {
jobImageView.loadImageUsingCacheWithUrlString(post.imageUrl1!)
jobLabel.text = post.category
addressLabel.text = post.category
}
}
}
fileprivate func setupView(){
let postUser = workerUser.self
let uid = Auth.auth().currentUser?.uid
let userName = postUser?.name
let posterId = postUser?.uid
let post = notification?.poster
guard let userImage = workerUser?.profileImageUrl else { return }
Database.database().reference().child("users").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String : Any] else { return }
let user = User(dictionary: dictionary as [String : AnyObject])
let currentUser = MyUser(dictionary: dictionary as [String : AnyObject])
self.posterImageView.image = #imageLiteral(resourceName: "user")
self.posterImageView.loadImageUsingCacheWithUrlString(userImage)
self.userNameLabel.text = userName
self.userNameLabel.font = UIFont.systemFont(ofSize: 30)
self.userNameLabel.textColor = UIColor.black
self.workerImageView.image = #imageLiteral(resourceName: "user")
self.workerImageView.loadImageUsingCacheWithUrlString(currentUser.profileImageUrl!)
self.workerNameLabel.text = currentUser.name
self.workerNameLabel.font = UIFont.systemFont(ofSize: 30)
self.workerNameLabel.textColor = UIColor.black
self.addressLabel.text = postUser?.address
self.addressLabel.font = UIFont.systemFont(ofSize: 30)
self.addressLabel.textColor = UIColor.black
self.jobLabel.text = post?.category
self.jobLabel.font = UIFont.systemFont(ofSize: 30)
self.jobLabel.textColor = UIColor.black
}, withCancel: { (err) in
print("attempting to load information")
})
print("this is your uid \(posterId!)")
}
below is how I populate my notificationCell which shows the users information in my tableView
var jobProgressView: jobProgressViewController? = nil
var delegate: NotificationCellDelegate?
var notification: userNotifications? {
didSet {
guard let user = notification?.user else { return }
guard let profileImageUrl = user.profileImageUrl else { return }
profileImageView.loadImageUsingCacheWithUrlString(profileImageUrl)
configureNotificationLabel()
configureNotificationType()
if let post = notification?.poster {
postImageView.loadImageUsingCacheWithUrlString(post.imageUrl1!)
}
}
}
func configureNotificationLabel() {
guard let notification = self.notification else { return }
guard let user = notification.user else { return }
guard let poster = notification.poster else { return }
guard let username = user.name else { return }
guard let notificationDate = configureNotificationTimeStamp() else { return }
guard let jobName = poster.category else { return }
let notificationMessage = notification.notificationType.description
let attributedText = NSMutableAttributedString(string: username, attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14)])
attributedText.append(NSAttributedString(string: notificationMessage , attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14), NSAttributedString.Key.foregroundColor: UIColor.black]))
attributedText.append(NSAttributedString(string: jobName, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14), NSAttributedString.Key.foregroundColor: UIColor.black]))
attributedText.append(NSAttributedString(string: " \(notificationDate).", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14), NSAttributedString.Key.foregroundColor: UIColor.gray]))
notificationLabel.attributedText = attributedText
}
if there is anyInformation I may have left out to help with getting an answer please let me know. please and thank you.
As discusstion, you forget set notification for jobProgressViewController
In func didSelectRowAt indexPath add below code:
acceptWorker.notification = notification
This is the full code for the chatViewController. Every time when I click send button, the text messages will have some duplicate and fly out of text box as shown in this picture.
However, when I exit the chat and re-enters,those message that fly out of the box are gone. It is not because of duplicated messages in Array, I believe it should be due to either Cache issue or the updating mechanism is wrong.
Every time when I close chat and re-enter, the fly out of text box chat does not exist, everything is fine. If I do not click on send Message,the Messages all reload fine.
It is only when I click send message, there will be a few (not all) messages that duplicate and fly out of box. When I exit the chat, and re-enter,the out of box chat will be gone and everything is normal.
I consulted with other programmers who saw my code and said it is most likely because when you click send button, it fetches complete dataset, and when it display on your screen, the old data on your screen (even though you have removed them from your array) messed up with the new data. I asked them how to solve it, they are not sure because they are Android developers.
A train of thought I was thinking: cache the old messages, add the new messages but make sure they do not conflict with the old messages in the cache?
import UIKit
import Firebase
import FirebaseStorage
import FirebaseDatabase
import FirebaseFirestore
import CoreData
class ChatViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var messageText: UITextField!
#IBAction func back(_ sender: Any) {
back()
}
#IBOutlet weak var profileImage: UIImageView!
#IBOutlet weak var usernameLabel: UILabel!
#IBAction func sendTextMessage(_ sender: Any) {
chats.removeAll()
self.sendDataToDatabase(message: messageText.text!)
messageText.text = nil
self.loadPosts()
self.loadPostsReceivedMessage()
delayCompletionHandler {
self.collectionView.reloadData()
}
}
let cellIdentifier = "chatNow"
var chats = [Chat]()
var posts = [Post]()
var receiverIDNumber = ""
var profileImageUrl = ""
var senderString = ""
var conversationsCounterInt = 0
var timestamp = String(Int(Date().timeIntervalSince1970))
let db = Firestore.firestore()
override func viewDidLoad() {
super.viewDidLoad()
let yourNibName = UINib(nibName: "ChatCollectionViewCell", bundle: nil)
collectionView.register(yourNibName, forCellWithReuseIdentifier: cellIdentifier)
self.collectionView.dataSource = self
self.collectionView.delegate = self
print(receiverIDNumber)
usernameLabel.text! = receiverIDNumber
setUpProfile()
set()
print("is there anything"+profileImageUrl)
//Making circular profile picture
profileImage.layer.borderWidth = 1.0
profileImage.layer.masksToBounds = false
profileImage.layer.borderColor = UIColor.white.cgColor
profileImage.layer.cornerRadius = profileImage.frame.size.width / 2
profileImage.clipsToBounds = true
print("waka"+senderString)
self.loadPosts()
self.loadPostsReceivedMessage()
delayCompletionHandler {
self.collectionView.reloadData()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.title = "Chat"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func back() {
let storyboard = UIStoryboard(name: "Main" , bundle: nil)
let tabViewController = storyboard.instantiateViewController(withIdentifier: "tab")
present(tabViewController, animated: true,completion: nil)
}
func set() {
let uid = Auth.auth().currentUser?.uid
let ref = Database.database().reference()
ref.child("users").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
if let dic = snapshot.value as? [String: AnyObject] {
self.senderString = dic["username"] as! String
print("this"+self.senderString)
}
})
}
func sendDataToDatabase(message: String){
let ref = Database.database().reference()
let senderIDNumber = Auth.auth().currentUser?.uid
let timeStampString = String(Int(Date().timeIntervalSince1970))
db.collection("chats").addDocument(data: ["message": messageText.text!, "senderID": senderIDNumber!,"receiverID": receiverIDNumber,"timestamp": timeStampString,"profileUrl": profileImageUrl, "sender": self.senderString])
{ err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func logout(){
let storyboard = UIStoryboard(name: "Main" , bundle: nil)
let loginViewController = storyboard.instantiateViewController(withIdentifier: "login")
present(loginViewController, animated: true,completion: nil)
}
func delayCompletionHandler(completion:#escaping (() -> ())) {
let delayInSeconds = 0.3
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {
completion()
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int{
return chats.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ChatCollectionViewCell
let senderIDNumber = Auth.auth().currentUser?.uid
//Setup the messageReceived and messageSent
if chats[indexPath.row].senderID == senderIDNumber {
if let chatsText = chats[indexPath.row].message{
let size = CGSize(width: 250, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: chatsText).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 18)], context: nil)
cell.messageSend.frame = CGRect(x:8,y:0,width:estimatedFrame.width + 16, height:estimatedFrame.height + 20)
cell.textBubbleView.frame = CGRect(x:0,y:0,width:estimatedFrame.width + 16 + 8, height:estimatedFrame.height + 20)
//showOutgoingMessage(text: chats[indexPath.row].message)
cell.messageSend.text = chats[indexPath.row].message
}
}
else {
cell.messageReceived.text = chats[indexPath.row].message
let chatsText = chats[indexPath.row].message
let size = CGSize(width: 250, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: chatsText!).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 18)], context: nil)
cell.messageReceived.frame = CGRect(x:view.frame.width - estimatedFrame.width - 30,y:0,width:estimatedFrame.width + 16, height:estimatedFrame.height + 20)
cell.textBubbleView.frame = CGRect(x:view.frame.width - estimatedFrame.width - 30,y:0,width:estimatedFrame.width + 16 + 4, height:estimatedFrame.height + 20)
}
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
if let chatsText = chats[indexPath.row].message {
let size = CGSize(width: 250, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let estimatedFrame = NSString(string: chatsText).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font : UIFont.systemFont(ofSize: 18)], context: nil)
return CGSize(width: view.frame.width, height: estimatedFrame.height + 20)
}
return CGSize(width: view.frame.width, height: 200)
}
//Get Message sent
func loadPosts() {
let senderIDNumber = Auth.auth().currentUser?.uid
let chatsRef = db.collection ("chats").order (by: "timestamp", descending: false)
let sentListener = chatsRef.whereField ("senderID", isEqualTo: senderIDNumber!)
.whereField ("receiverID", isEqualTo: receiverIDNumber)
.addSnapshotListener() {
querySnapshot,
error in
guard let documentChanges = querySnapshot?.documentChanges else {
print ("Error fetching documents: \(error!)")
return
}
for documentChange in documentChanges {
if (documentChange.type == .added) {
let data = documentChange.document.data ()
print("Message send: \(data)")
let messageText = data["message"] as? String
let senderIDNumber = data["senderID"] as? String
let receiverIDNumber = data["receiverID"] as? String
let timestamp = data["timestamp"] as? String
guard let sender = data["sender"] as? String else {return}
// let conversationsCounter = document.data()["conversationsCounter"] as? Int
guard let profileUrl = data["profileUrl"] as? String else { return}
let chat = Chat(messageTextString: messageText!, senderIDNumber: senderIDNumber!, receiverIDNumber: receiverIDNumber!, timeStampString: timestamp!, profileImageUrl: profileUrl, senderString: sender)
self.chats.append(chat)
print(self.chats)
}
}
}
}
//Get message received
func loadPostsReceivedMessage() {
let chatsRef = db.collection("chats").order(by: "timestamp", descending: false)
print("thecurrentreceiver"+senderString)
print("thecurrentsender"+receiverIDNumber)
let receivedListener = chatsRef.whereField("receiverID", isEqualTo: senderString).whereField("sender", isEqualTo: receiverIDNumber)
.addSnapshotListener() {
querySnapshot,
error in
guard let documentChanges = querySnapshot?.documentChanges else {
print ("Error fetching documents: \(error!)")
return
}
for documentChange in documentChanges {
if (documentChange.type == .added) {
let data = documentChange.document.data ()
print("Message received: \(data)")
let messageText = data["message"] as? String
let senderIDNumber = data["senderID"] as? String
let receiverIDNumber = data["receiverID"] as? String
let timestamp = data["timestamp"] as? String
guard let sender = data["sender"] as? String else {return}
// let conversationsCounter = document.data()["conversationsCounter"] as? Int
guard let profileUrl = data["profileUrl"] as? String else { return}
let chat = Chat(messageTextString: messageText!, senderIDNumber: senderIDNumber!, receiverIDNumber: receiverIDNumber!, timeStampString: timestamp!, profileImageUrl: profileUrl, senderString: sender)
self.chats.append(chat)
print(self.chats)
self.chats.sort{$0.timestamp < $1.timestamp}
}
}
}
}
func setUpProfile() {
guard let url = URL(string: profileImageUrl) else { return}
let task = URLSession.shared.dataTask(with: url){ data, reponse, error in
if error != nil {
print(error!)
} else{
DispatchQueue.main.async(execute: {
self.profileImage.image = UIImage(data: data!)
})
}
}
task.resume()
}
}
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>()
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
}
I'm developing a chat app, I'm having problem showing the Avatar to my JSQMessagesViewController
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
var avatar = UIImage()
let people = FIRDatabase.database().reference().child("people").child(senderId)
people.observeEventType(.Value, withBlock: {
snapshot -> Void in
let dict = snapshot.value as! Dictionary<String, AnyObject>
let imageUrl = dict["profileImage"] as! String
if imageUrl.hasPrefix("gs://") {
FIRStorage.storage().referenceForURL(imageUrl).dataWithMaxSize(INT64_MAX, completion: { (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
avatar = UIImage.init(data: data!)!
})
}
})
let AvatarJobs = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(avatar, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
return AvatarJobs
}
The problem here is, when I'm trying to pull the image of the sender from firebase, I'm getting a blank image, but when i try to use this let AvatarJobs = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(UIImage(named: "icon.png"), diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault)) it's working fine, What do you think is the problem here? Thanks!
If I may suggest an alternative? Why don't you have a dictionary:
var avatars = [String: JSQMessagesAvatarImage]()
let storage = FIRStorage.storage()
And use the following function:
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource!
{
let message = messages[indexPath.row]
return avatars[message.senderId]
}
And create the avatars in viewDidLoad (or where ever )
createAvatar(senderId, senderDisplayName: senderDisplayName, user: currentUser, color: UIColor.lightGrayColor())
with a function
func createAvatar(senderId: String, senderDisplayName: String, user: FBUser, color: UIColor)
{
if self.avatars[senderId] == nil
{
//as you can see, I use cache
let img = MyImageCache.sharedCache.objectForKey(senderId) as? UIImage
if img != nil
{
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(img, diameter: 30)
// print("from cache")
}
else if let photoUrl = user.pictureURL where user.pictureURL != ""
{
// the images are very small, so the following methods work just fine, no need for Alamofire here
if photoUrl.containsString("https://firebasestorage.googleapis.com")
{
self.storage.referenceForURL(photoUrl).dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if (error != nil)
{
//deal with error
}
else
{
let newImage = UIImage(data: data!)
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage!, forKey: senderId, cost: data!.length)
}
}
}
else if let data = NSData(contentsOfURL: NSURL(string:photoUrl)!)
{
let newImage = UIImage(data: data)!
self.avatars[senderId] = JSQMessagesAvatarImageFactory.avatarImageWithImage(newImage, diameter: 30)
MyImageCache.sharedCache.setObject(newImage, forKey: senderId, cost: data.length)
}
else
{
//etc. blank image or image with initials
}
}
}
else
{
//etc. blank image or image with initials
}
}
for Cache I have a custom class
import Foundation
class MyImageCache
{
static let sharedCache: NSCache =
{
let cache = NSCache()
cache.name = "MyImageCache"
cache.countLimit = 200 // Max 200 images in memory.
cache.totalCostLimit = 20*1024*1024 // Max 20MB used.
return cache
}()
}
Let me know if that helps
I would suggest trying to isolate your problems. I don't know if the issue is with JSQMessagesAvatarImageFactory I think the issue may be that you do not have the image downloaded by the time the cell needs to be displayed. I would make sure that you are getting something back from fireBase before you try and set it to your avatar. A closure is normally how I do this something like
func getImageForUser(id: String, completiion() -> Void) {
//Add your logic for retrieving from firebase
let imageFromFirebase = firebaserReference.chiledWithID(id)
completion(image)
}
Then in your
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
var avatarImage = JSQAavatarImage()
getImageForUser {
self.avatarImage = JSQMessagesAvatarImageFactory.avatarImageWithPlaceholder(imageFromFirebase, diameter: UInt(kJSQMessagesCollectionViewAvatarSizeDefault))
self.collectionView.reloadItemAtIndexPath(indexPath)
}
That way it waits till the response is back and then reloads the cell once it is there.
Let me know if you have any other questions.