I am implementing a social media application using Swift. In the MyProfileViewController, I have used UIImagePickerController to change the current user's profile picture. However, usage of UIImagePickerController causes a duplicate of table view rows even though I handle the table view in viewDidLoad, not in viewWillAppear.
To illustrate, here is my viewDidLoad function.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
self.automaticallyAdjustsScrollViewInsets = false
ref = Database.database().reference()
storageRef = Storage.storage().reference()
ref.child("users").child((Auth.auth().currentUser?.uid)!).observe(.value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let userId = value?["id"] as! String
let username = value?["username"] as! String
let email = value?["userEmail"] as! String
let profilePictureUrl = value?["profilePicture"] as! String
self.currentUser = User(id: userId, username: username, email: email, profilePicture: profilePictureUrl)
self.tableView.reloadData()
})
ref.child("posts").observe(.childAdded, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let id = value?["id"] as! String
let tags = value?["tags"] as! String
let facultyName = value?["faculty"] as! String
let courseName = value?["course"] as! String
let questionTitle = value?["title"] as! String
let questionText = value?["description"] as! String
let dateAndTime = value?["dateAndTime"] as! String
let userID = value?["user-id"] as! String
self.ref.child("users").child(userID).observe(.value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let userId = value?["id"] as! String
let username = value?["username"] as! String
let email = value?["userEmail"] as! String
let profilePictureUrl = value?["profilePicture"] as! String
let post = Post(id: id, tags: tags, facultyName: facultyName, courseName: courseName, questionTitle: questionTitle, questionText: questionText, dateAndTime : dateAndTime, username: username)
if userID == Auth.auth().currentUser?.uid {
self.posts.insert(post, at: 0)
}
self.postDictionary[id] = post
let member = User(id: userId, username: username, email: email, profilePicture: profilePictureUrl)
if post.username == member.username {
self.userDictionary[id] = member
}
self.tableView.reloadData()
})
})
}
And here is my UIImagePickerController function:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let indexPath = NSIndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: indexPath as IndexPath) as! MyProfileInfoCell!
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
selectedImage = image
cell?.profilePicture.contentMode = .scaleAspectFit
cell?.profilePicture.image = image
}
var data = NSData()
data = UIImageJPEGRepresentation((cell?.profilePicture.image!)!, 0.8)! as NSData
let filePath = "\((Auth.auth().currentUser?.uid)!)" // path where you wanted to store img in storage
let metaData = StorageMetadata()
metaData.contentType = "image/jpg"
self.storageRef = Storage.storage().reference()
self.storageRef.child(filePath).putData(data as Data, metadata: metaData) {
(metaData,error) in
if let error = error {
print(error.localizedDescription)
return
} else{
let downloadURL = metaData!.downloadURL()!.absoluteString
self.ref.child("users").child((Auth.auth().currentUser?.uid)!).updateChildValues(["profilePicture" : downloadURL])
}
}
self.dismiss(animated: true, completion: nil)
print(self.posts.count)
}
My table view consists of 2 prototype cells. The first prototype cell is for the profile information, in which changing profile picture is handled. The second prototype cell is for showing the posts posted by that user. The problem occurs here. When I change my profile picture, the posts rows are being duplicated.
Can anyone help me with this?
The observe method adds an observer to "users" on your DB. Every time you changes the "users" on DB the following code is executed and a post is added to tableview:
self.ref.child("users").child(userID).observe(.value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let userId = value?["id"] as! String
let username = value?["username"] as! String
let email = value?["userEmail"] as! String
let profilePictureUrl = value?["profilePicture"] as! String
let post = Post(id: id, tags: tags, facultyName: facultyName, courseName: courseName, questionTitle: questionTitle, questionText: questionText, dateAndTime : dateAndTime, username: username)
if userID == Auth.auth().currentUser?.uid {
self.posts.insert(post, at: 0)
}
self.postDictionary[id] = post
let member = User(id: userId, username: username, email: email, profilePicture: profilePictureUrl)
if post.username == member.username {
self.userDictionary[id] = member
}
self.tableView.reloadData()
})
When you update the profile picture the DB is updated too and the above code is executed. That is why the posts are duplicated. To fix that changes the method from observe to observeSingleEvent. The observeSingleEvent method requests data from DB once.
self.ref.child("users").child(userID).observeSingleEvent(.value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let userId = value?["id"] as! String
let username = value?["username"] as! String
let email = value?["userEmail"] as! String
let profilePictureUrl = value?["profilePicture"] as! String
let post = Post(id: id, tags: tags, facultyName: facultyName, courseName: courseName, questionTitle: questionTitle, questionText: questionText, dateAndTime : dateAndTime, username: username)
if userID == Auth.auth().currentUser?.uid {
self.posts.insert(post, at: 0)
}
self.postDictionary[id] = post
let member = User(id: userId, username: username, email: email, profilePicture: profilePictureUrl)
if post.username == member.username {
self.userDictionary[id] = member
}
self.tableView.reloadData()
})
Related
I'm new to firebase and I wanted to try using a realtime database. It's been fairly easy to use until it's time to fetch the data from the database. I'm currently stuck at trying to get the data back into an array to provide my tableView with data.
Each image is saved with a timestamp. I've fetching it in different ways, it ends up printing nil or breaking the app. But when i use a breakpoint, I'm able to see the same data.
func downloadFromFirebase(completion: #escaping (Bool, Error?) -> Void) {
ref.child("ImageDetails/").observeSingleEvent(of: .value) { (snapshot) in
guard let value = snapshot.value as? [String:Any] else { return }
let name = value["username"] as! String
completion(true, nil)
}
}
I don't understand your question so I will write a code in order to retrieve the data from realtime database and to save it in an array.
You said "Each image is saved with a timestamp", so I assume you want to retrieve an array of data containing images(imageURL, etc) right?
we create a structure based on your database
struct dataStructure {
var Description:String?
var imageURL:String?
var Portfolio:String?
var createdAt:String?
var Instagram:String?
var profileImageUrl:String?
var Twitter:String?
var Username:String?
}
// now we create an array of this struct
var arrayOfData = [dataStructure]()
func downloadFromFirebase(completion:#escaping (Bool ,Error?)-> Void) {
ref.child("ImageDetails/").observeSingleEvent(of: .value) { snapshot in
guard let value = snapshot.value as? [[String:Any]] else { return }
// we add each element of value in the arrayOfData
for element in value {
guard let name = value["username"] as! String,
let Description = value["Description"] as! String,
let imageURL = value["imageURL"] as! String,
let Portfolio = value["Portfolio"] as! String,
let creationDate = value["createdAt"] as! String,
let Instagram = value["Instagram"] as! String,
let profileImageUrl = value["profileImageUrl"] as! String,
let Twitter = value["Twitter"] as! String ,
let Username = value["Username"] as! String else {
completion(false)
return
}
arrayOfData.append(dataStructure(name: name,
Description: Description,
imageURL: imageURL,
Portfolio: Portfolio ,
creationDate:creationDate ,
Instagram: Instagram,
profileImageUrl: profileImageUrl,
Twitter: Twitter))
}
completion(true)
}
}
at the end you will have an array with all your data
here is the code if you want to take the data from this function in your completion
func downloadFromFirebase(completion:#escaping (Result<dataStructure, Error>) {
ref.child("ImageDetails/").observeSingleEvent(of: .value) { snapshot in
guard let value = snapshot.value as? [[String:Any]] else { return }
// we add each element of value in the arrayOfData
for element in value {
guard let name = value["username"] as! String,
let Description = value["Description"] as! String,
let imageURL = value["imageURL"] as! String,
let Portfolio = value["Portfolio"] as! String,
let creationDate = value["createdAt"] as! String,
let Instagram = value["Instagram"] as! String,
let profileImageUrl = value["profileImageUrl"] as! String,
let Twitter = value["Twitter"] as! String ,
let Username = value["Username"] as! String else {
completion(.failure(error)
return
}
arrayOfData.append(dataStructure(name: name,
Description: Description,
imageURL: imageURL,
Portfolio: Portfolio ,
creationDate:creationDate ,
Instagram: Instagram,
profileImageUrl: profileImageUrl,
Twitter: Twitter))
}
/* we provide the array with the new data in the completion at the
end of the loop */
completion(.success(arrayOfData))
}
}
I fetch data and display in a tableView, the problem is the data is not executing in the correct order.
I have tried:
for case let child as DataSnapshot in data!.children.reversed() {
let newDispatchGroup = DispatchGroup()
let commentID = child.key
let uid = child.childSnapshot(forPath: "UID").value as! String
let commentText = child.childSnapshot(forPath: "Comment").value!
let timeStamp = child.childSnapshot(forPath: "timeStamp").value!
let date = ConvertDate(mediaTimestamp: timeStamp as! Double).getDate!
//print(date, "dsfsdafdasfdsafdsahjkfhfdsafsajkadhffdsfsafsasjkfhsdajkhfdsajkhfjklads")
newDispatchGroup.enter()
ref.child("users2").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot, "dshjkfhjkadhfsjkfhsdajkhfdsajkhfjklads")
print(date, "dsfsdafdasfdsafdsahjkfhfdsafsajkadhffdsfsafsasjkfhsdajkhfdsajkhfjklads")
let username = snapshot.childSnapshot(forPath: "username").value
let profileImage = snapshot.childSnapshot(forPath: "profileImage").value
let newUser = User(theuserID: uid, theUsername: username as! String, theprofImage: profileImage as! String)
let newComment = Comment(newUser: newUser, text: commentText as! String, timeStamp: date, NcommentID: commentID)
self.commentsVC1.arrayOfComments.append(newComment)
newDispatchGroup.leave()
//completion()
})
newDispatchGroup.notify(queue: .main, execute: {
print(self.totalComments, "COgfdsdfgfdsgdsfgdfsgfdsgdfsgdskj", self.commentsVC1.arrayOfComments.count)
if self.totalComments == self.commentsVC1.arrayOfComments.count {
print("COmejkfbdshkafdsagfhksdagfdsakj")
self.commentsVC1.tableView.reloadData()
}
})
}
})
}
But it did not work either, the order in which the second firebase calls execute is incorrect.
You should set up your notify closure when you set up your DispatchGroup. And you would not need to use a completion closure for you loadComments function.
let dispatchGroup = DispatchGroup()
dispatchGroup.notify(queue: .main, execute: {
if self.totalComments == self.commentsVC1.arrayOfComments.count {
print("COmejkfbdshkafdsagfhksdagfdsakj")
self.commentsVC1.tableView.reloadData()
}
})
loadComments()
notify will be called, when leave has been called the same amount of times as enter. In your code the last leave call is happening before you have set the anything to be notified about.
I solved with this:
for case let child as DataSnapshot in snap.children.reversed() {
let commentID = child.key
let uid = child.childSnapshot(forPath: "UID").value as! String
let commentText = child.childSnapshot(forPath: "Comment").value!
let timeStamp = child.childSnapshot(forPath: "timeStamp").value!
let date = ConvertDate(mediaTimestamp: timeStamp as! Double).getDate!
let newUser = User(theuserID: uid)
let newComment = Comment(newUser: newUser, text: commentText as! String, timeStamp: date, NcommentID: commentID)
self.commentsVC1.arrayOfComments.append(newComment)
ref.child("users2").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
let username = snapshot.childSnapshot(forPath: "username").value
let profileImage = snapshot.childSnapshot(forPath: "profileImage").value
let newUserIner = User(theuserID: uid, theUsername: username as! String, theprofImage: profileImage as! String)
newComment.user = newUserIner
if self.totalComments == self.commentsVC1.arrayOfComments.count {
self.commentsVC1.tableView.reloadData()
}
})
}
I would use dispatch group here though so one does not have have to check if its done unnecessarily.
I have an app where users can like posts and I want to determine if the current user has previously liked a post in an efficient manner. My data currently looks like this:
I also store the likes for every user
In my current query I am doing this:
if let people = post["peopleWhoLike"] as? [String: AnyObject] {
if people[(Auth.auth().currentUser?.uid)!] != nil {
posst.userLiked = true
}
}
However, I believe this requires me to download all of the post's likes which isn't very efficient, so I tried this:
if (post["peopleWhoLike\(Auth.auth().currentUser!.uid)"] as? [String: AnyObject]) != nil {
posst.userLiked = true
}
The second method doesn't seem to be working correctly. Is there a better way to do this?
Here is my initial query as well:
pagingReference.child("posts").queryLimited(toLast: 5).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 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, let numLikes = post["likes"] as? Int {
Solved it:
In the tableView I just query for the liked value directly and then determine what button to display.
static func userLiked(postID: String, cell: BetterPostCell, indexPath: IndexPath) {
// need to cache these results so we don't query more than once
if newsfeedPosts[indexPath.row].userLiked == false {
if let uid = Auth.auth().currentUser?.uid {
likeRef.child("userActivity").child(uid).child("likes").child(postID).queryOrderedByKey().observeSingleEvent(of: .value, with: { snap in
if snap.exists() {
newsfeedPosts[indexPath.row].userLiked = true
cell.helpfulButton.isHidden = true
cell.notHelpfulButton.isHidden = false
}
})
likeRef.removeAllObservers()
}
}
}
Called in my TableView:
DatabaseFunctions.userLiked(postID: newsfeedPosts[indexPath.row].postID, cell: cell, indexPath: indexPath)
I have this function that it will be triggered every time a user changes his picture, and will get the profilePictureUrl of the current user and update all posts matching the currentUser in the database. Works fine! Is doing the job. How do I update the indexPath of those particular cells? Right now my cells images are empty after the update.
var pathToPictures = [Pictures]()
func updateAllImagesOfCurrentUserInDatabase() {
//refrences
let ref = FIRDatabase.database().reference()
let uid = FIRAuth.auth()?.currentUser?.uid
//match the user ID value stored into posts with current userID and get all the posts of the user
let update = ref.child("posts").queryOrdered(byChild: "userID").queryEqual(toValue: uid)
update.observe(FIRDataEventType.value, with: { (snapshot) in
self.pathToPictures.removeAll()
if snapshot.value as? [String : AnyObject] != nil {
let results = snapshot.value as! [String : AnyObject]
for (_, value) in results {
let pathToPostPicture = Pictures()
if let pathToImage = value["pathToImage"] as? String , let postID = value["postID"] as? String {
pathToPostPicture.postImageUrl = pathToImage
pathToPostPicture.postID = postID
self.pathToPictures.append(pathToPostPicture)
print("Image and POST ID: \(pathToPostPicture.postImageUrl!)")
print("Post ID is : \(postID)")
if FIRAuth.auth()?.currentUser?.uid == uid {
ref.child("Users_Details").child(uid!).child("profileImageUrl").observeSingleEvent(of: .value, with: { (userSnapshot) in
print(userSnapshot)
let userIMageUrl = userSnapshot.value as! String
pathToPostPicture.postImageUrl = userIMageUrl
self.pathToPictures.append(pathToPostPicture)
print("This is the image path:" + userIMageUrl + String(self.pathToPictures.count))
// Update the Database
let postIDCount = pathToPostPicture.postID!
let updatedUserData = ["posts/\(postIDCount)/pathToImage": pathToPostPicture.postImageUrl!]
print("This is THE DATA:" , updatedUserData)
ref.updateChildValues(updatedUserData as Any as! [AnyHashable : Any])
})
}
print(self.pathToPictures.count)
}
}
} else {
print("snapshot is nill - add some data")
}
self.collectionView?.reloadData()
})
}
I'm trying to fill the collectionView with posts. I have to get the posts, then get some data for the users who posted them. For some reason it isn't working.
DataService.ds.REF_POSTS.child("\(self.loggedInUser!.uid)").queryLimitedToLast(30).observeSingleEventOfType(.Value, withBlock: { postDictionary in
if postDictionary.exists() {
if let snapshots = postDictionary.children.allObjects as? [FIRDataSnapshot] {
self.posts = [Post]()
for snap in snapshots {
if let postDict = snap.value as? NSDictionary {
for(name, value) in postDict {
let interval = postDict.objectForKey("timePosted") as! Double
let formattedDate = NSDate(timeIntervalSince1970: interval)
let timeAgo = self.getDate(formattedDate)
if name as! String == "postedBy" {
DataService.ds.REF_USERS.child(value as! String).observeSingleEventOfType(.Value, withBlock: { (userDictionary) in
let userDict = userDictionary.value as! NSDictionary
let username = userDict.objectForKey("username")!
let profileThumbUrl = userDict.objectForKey("profileThumbUrl")!
let key = snap.key
let post = Post(postKey: key, dictionary: postDict, username: username as! String, profileThumbUrl: profileThumbUrl as! String, timeAgo: timeAgo)
self.posts.append(post)
})
}
}
}
}
}
}
self.collectionView?.reloadData()
})
It works if I perform the reload() right after appending the posts, but there is some sort of memory leak. There isn't a problem in the Post class or filling the collection view, if I use dummy values. The problem is in this code that I posted. I think I have an extra loop or something can anyone help?
If you fear that reload() is the reason of the memory leak , you can use this hack:-
if name as! String == "postedBy" {
DataService.ds.REF_USERS.child(value as! String).observeSingleEventOfType(.Value, withBlock: { (userDictionary) in
let userDict = userDictionary.value as! NSDictionary
let username = userDict.objectForKey("username")!
let profileThumbUrl = userDict.objectForKey("profileThumbUrl")!
let key = snap.key
let post = Post(postKey: key, dictionary: postDict, username: username as! String, profileThumbUrl: profileThumbUrl as! String, timeAgo: timeAgo)
self.posts.append(post)
if posts.count == postDictionary.childrenCount{
self.collectionView?.reloadData()
}
})
}
Then also see this answer :- Firebase observeSingleEventOfType stays in memory