Firebase pagination with CollectionView always loads the same image - swift

I am trying to implement a pagination in my CollectionView.
Right now, I always get the same picture, when I call my function loadMorePosts
I am sure it is because I don't revert the snapshot of my data, so the image will always be on top.
How do I reverse my snapshot to get the correct data?
This is my code to add new data:
func loadMorePosts() {
var numOfItems = posts.count
let preNumOfItems = numOfItems
numOfItems += 1
let REF_POST = Database.database().reference().child("posts")
let REF_QUERY = REF_POST.queryOrdered(byChild: "postDate").queryLimited(toLast: UInt(numOfItems))
REF_QUERY.observe(.value) { (snapshot) in
var newPost = [PostModel]()
var count = 0
for post in snapshot.children {
let data = post as! DataSnapshot
print("num of items " + String(numOfItems))
print("counter " + String(count))
let lostPost = PostModel(dictionary: data.value as! [String : Any], key: snapshot.key)
if count >= Int(preNumOfItems) {
newPost.insert(lostPost, at: 0)
}
count+=1
}
self.posts += newPost
self.exploreCollectionView.reloadData()
}
}

Related

Reload TableView only at new rows once user scrolls to bottom

I am trying to append new data to Tableview and only reload the Tableview for the new data added.
My app has been crashing giving me this error: "attempt to delete row 19 from section 0 which only contains 10 rows before the update'"
I would like to reload those rows once the asynchronous function contentQueryContinuous() is completed.
Here is my code:
//Once User Scrolls all the way to bottom, beginContentBatchFetch() is called
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentSize.height
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
beginContentBatchFetch()
}
}
//Content Batch Fetch looks up to 10 new elements, tries to append to current array's, and then reload's tableview only for those new elements
func beginContentBatchFetch() {
contentFetchMore = true
ProgressHUD.show()
let oldcount = contentObjectIdArray.count
var IndexPathsToReload = [IndexPath]()
var startIndex = Int()
var endIndex = Int()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
//Calls self.contentQueryContinous which adds new elements
self.contentQueryContinous()
let newElements = self.contentObjectIdArray.count - oldcount
startIndex = oldcount
endIndex = self.contentObjectIdArray.count
for index in startIndex..<endIndex {
let indexPath = IndexPath(row: index, section: 0)
IndexPathsToReload.append(indexPath)
}
if newElements > 0 {
self.MyTableView.reloadRows(at: IndexPathsToReload, with: .fade)
}
ProgressHUD.dismiss()
}
}
Here is contentQueryContinous()
func contentQueryContinous() {
if contentObjectIdArray.count != 0 {
contentSkip = contentObjectIdArray.count
let query = PFQuery(className: "ContentPost")
query.whereKey("Spot", equalTo: SpotText)
query.limit = 10
query.skip = contentSkip
query.addDescendingOrder("createdAt")
query.findObjectsInBackground(block: { (objects: [PFObject]?,error: Error?) in
if let objects = objects {
for object in objects {
let ProfileImageFile = object["ProfileImage"] as? PFFileObject
let urlString = ProfileImageFile?.url as! String
if let url = URL(string: urlString) {
let data = try? Data(contentsOf: url)
if let imageData = data {
self.contentPostProPicUrlArray.append(urlString as NSString)
self.contentPostProPicImageCache.setObject(UIImage(data: imageData)!, forKey: urlString as NSString)
}
}
if object["Post"] != nil && object["UserLikes"] != nil && object["Username"] != nil && object["UserTime"] != nil {
self.contentPostArray.append(object["Post"] as! String)
self.contentLikeArray.append(object["UserLikes"] as! Int)
self.contentUsernameArray.append(object["Username"] as! String)
self.contentTimeArray.append(object["UserTime"] as! String)
self.contentObjectIdArray.append(object.objectId!)
}
}
print(self.contentPostArray)
}
})
}
}
You are trying to reload a cell that is not on the table. Try inserting it. Also, you are risking a race condition.
From the docs
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing.

Cant fetch SavedPost

My app has an option to save posts for users to watch later. The code is:
func savedPost(for cell: FirstView) {
guard let indexPath = collectionView.indexPath(for: cell) else { return }
var post = self.posts[indexPath.item]
guard let currentuserId = Auth.auth().currentUser?.uid else { return }
let targetUid = post.user.uid
guard let postId = post.id else { return }
let ref = Database.database().reference().child("save_post").child(currentuserId).child(postId)
if post.hasSaved {
ref.removeValue { (err, _) in
if let _ = err {
showErr(info: NSLocalizedString("failtoUnsave", comment: ""), subInfo: tryLater)
return
}
post.hasSaved = false
self.posts[indexPath.item] = post
self.collectionView.reloadItems(at: [indexPath])
}
} else {
let values = ["userId": targetUid]
ref.updateChildValues(values) { (err, ref) in
if let _ = err {
showErr(info: NSLocalizedString("failtoSave", comment: ""), subInfo: tryLater)
}
post.hasSaved = true
self.posts[indexPath.item] = post
self.collectionView.reloadItems(at: [indexPath])
}
}
}
With this code my firebase database in "save_post" has -> currentUseruId -> postid -> postUserId.
On ProfileController users can view saved Posts from "savedPost" Tab. The code is:
var savedPosts = [Post]()
fileprivate func fetchSaved() {
var userIds = [String]()
var postIds = [String]()
guard let uid = self.user?.uid else { return }
let getIDsRef = Database.database().reference().child("save_post").child(uid)
let query = getIDsRef.queryOrderedByKey()
query.observeSingleEvent(of: .value) { (snapshot) in
let dictionary = snapshot.value as? [String: Any]
dictionary?.forEach({ (key,value) in
guard let dic = value as? [String: String] else { return }
postIds.append(key)
userIds.append(dic["userId"] ?? "")
})
var i = 0
while i < userIds.count {
self.fetchPostsWithUserIDPostID(userID: userIds[i], postID: postIds[i])
i += 1
}
}
}
fileprivate func fetchPostsWithUserIDPostID(userID: String, postID: String) {
let getPostRef = Database.database().reference().child("video_list")
getPostRef.child(userID).child(postID).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
let ref = Database.database().reference().child("users").child(userID)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let user = User(uid: userID, dictionary: dict)
var post = Post(user: user, dictionary: dictionary)
post.id = postID
guard let currentUserUID = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("likes").child(postID).child(currentUserUID).observeSingleEvent(of: .value, with: { (snapshot) in
if let value = snapshot.value as? Int, value == 1 {
post.hasLiked = true
} else {
post.hasLiked = false
}
post.hasSaved = true
self.savedPosts.append(post)
self.savedPosts.sort(by: { (p1, p2) -> Bool in
return p1.creationDate.compare(p2.creationDate) == .orderedDescending
})
self.collectionView.reloadData()
})
})
})
}
However, when I click "savedPost" tab, there is no post shown. I don't know where my mistake is. I have all the necessary code under all override func collectionView(....). I believe the error should come from the code listed above. I am sincerely looking for help to resolve this issue. Thanks.
There could be a number of things going on here. It would be good to throw some print statements in there to make sure that 1) the data you're getting back from the database looks like what you expect, and 2) that you're properly parsing it into Post objects. Do you have your cells defined properly for your CollectionView? Also, I don't see where you are defining the data source for the CollectionView.

How can I retrieve users posts, add them with snapshots to table view - Swift

So I am trying to retrieve users follower information within an array. Then with that array get each users posts and then append them in my table view. All throughout this, I would like a snapshot listener to be added so that if a user likes a post the number will auto update. When I do this tho it appends every single update so one post will be shown about 5 times after an action such as liking it is performed which I do not want to happen. Could someone help me figure this out? I am using Xcode Swift. Thanks in advance!
class Posts {
var postArray = [UserPost]()
var db: Firestore!
init() {
db = Firestore.firestore()
}
func loadData(completed: #escaping () -> ()) {
let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
self.postArray = []
guard let user = Auth.auth().currentUser else { return }
let displayUsername = user.displayName
let userReference = db.collection("Users").document("User: \(displayUsername!)").collection("Connect").document("Following")
userReference.getDocument { (document, error) in
if let documentData = document?.data(),
var FollowerArray = documentData["Following"] as? [String] {
FollowerArray.append(displayUsername!)
FollowerArray.forEach {
self.db.collection("Users").document("User: \($0)").collection("Posts").whereField("timeOfPost", isGreaterThanOrEqualTo: sevenDaysAgo!)
.addSnapshotListener { (querySnapshot, error) in
guard error == nil else {
print("*** ERROR: adding the snapshot listener \(error!.localizedDescription)")
return completed()
}
//self.postArray = []
// there are querySnapshot!.documents.count documents in the posts snapshot
for document in querySnapshot!.documents {
let post = UserPost(dictionary: document.data())
self.postArray.append(post)
}
completed()
}
}
}
I would suggest a different approach and enable Firestore to tell you when child nodes (posts) have been added, modified or removed. Based on your code your structure is something like this:
Users
uid
//some user data like name etc
Posts
post_0
likes: 0
post: "some post 0 text"
post_1
likes: 0
post: "text for post 1"
Let's have a class to store the Post in
class UserPostClass {
var postId = ""
var postText = ""
var likes = 0
init(theId: String, theText: String, theLikes: Int) {
self.postId = theId
self.postText = theText
self.likes = theLikes
}
}
and then an array to hold the UserPosts which will be the tableView dataSource
var postArray = [UserPostClass]()
then.. we need a block of code to do three things. First, when a new post is added to the database (or when we first start the app), add it to the dataSource array. Second, when a post is modified, for example another user likes the post, update the array to reflect the new like count. Third, if a post is deleted, remove it from the array. Here's the code that does all three......
func populateArrayAndObservePosts() {
let uid = "uid_0" //this is the logged in user
let userRef = self.db.collection("users").document(uid)
let postsRef = userRef.collection("Posts")
postsRef.addSnapshotListener { documentSnapshot, error in
guard let snapshot = documentSnapshot else {
print("err fetching snapshots")
return
}
snapshot.documentChanges.forEach { diff in
let doc = diff.document
let postId = doc.documentID
let postText = doc.get("post") as! String
let numLikes = doc.get("likes") as! Int
if (diff.type == .added) { //will initially populate the array or add new posts
let aPost = UserPostClass(theId: postId, theText: postText, theLikes: numLikes)
self.postArray.append(aPost)
}
if (diff.type == .modified) { //called when there are changes
//find the post that was modified by it's postId
let resultsArray = self.postArray.filter { $0.postId == postId }
if let postToUpdate = resultsArray.first {
postToUpdate.likes = numLikes
}
}
if (diff.type == .removed) {
print("handle removed \(postId)")
}
}
//this is just for testing. It prints all of the posts
// when any of them are modified
for doc in snapshot.documents {
let postId = doc.documentID
let postText = doc.get("post") as! String
let numLikes = doc.get("likes") as! Int
print(postId, postText, numLikes)
}
}
}

How can I pick three random elements out of a dictionary in Swift 4.1

I am having a problem picking three random elements out of a dictionary.
My dictionary code:
query.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let childSnap = child as! DataSnapshot
var dict = childSnap.value as! [String: Any]
}
})
You can use an array if keys are integers.
if you want to use a dictionary only then below mentioned code might be helpful for you
var namesOfPeople = [Int: String]()
namesOfPeople[1] = "jacob"
namesOfPeople[2] = "peter"
namesOfPeople[3] = "sam"
func makeList(n: Int) -> [Int] {
print(namesOfPeopleCount)
return (0..<n).map { _ in namesOfPeople.keys.randomElement()! }
}
let randomKeys = makeList(3)
You can try this for older version Of Swift where randomElement() is not available
let namesOfPeopleCount = namesOfPeople.count
func makeList(n: Int) -> [Int] {
return (0..<n).map{ _ in Int(arc4random_uniform(namesOfPeopleCount)
}
#Satish answer is fine but here's one which is a bit more complete and selects a random user from a list of users loaded from Firebase ensuring a user is only selected once.
We have have an app with two buttons
populateArray
selectRandomUser
and we have a UserClass to store our user data for each user.
class UserClass {
var uid = ""
var name = ""
init(withSnapshot: DataSnapshot) {
let dict = withSnapshot.value as! [String: Any]
self.uid = withSnapshot.key
self.name = dict["Name"] as! String
}
}
and an array to store the users in
var userArray = [UserClass]()
When the populateArray button is clicked this code runs
func populateArray() {
let usersRef = self.ref.child("users")
usersRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let user = UserClass(withSnapshot: snap)
self.userArray.append(user)
}
print("array populated")
})
}
and then to select a random user use this code.
func selectRandomUser() {
if let someUser = userArray.randomElement() {
print("your random user: \(someUser.name)")
let uid = someUser.uid
if let index = userArray.index(where: { $0.uid == uid } ) {
userArray.remove(at: index)
}
} else {
print("no users remain")
}
}
This code ensures the same user is not selected twice. Note that this is destructive to the array containing the users so if that's unwanted, make a copy of the array after it's populated and work with that.

How to get follower and following count quickly with Firebase and Swift

I am currently trying to fetch all the followers for a specific user with firebase. In my didSet clause, I call the function setFollowingCount() to fetch the users that the current user follows and assign it to a text field:
var user: User? {
didSet {
setFollowingCount()
guard let following = self.user?.following else {return}
let attributedText = NSMutableAttributedString(string: "\(following)\n", attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 14)])
attributedText.append(NSAttributedString(string: "followers", attributes: [NSAttributedStringKey.foregroundColor: UIColor.lightGray, NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14)]))
self.followingLabel.attributedText = attributedText
}
}
The setFollowingCount() function is:
func setFollowingCount(){
var i = 0
guard let userId = self.user?.uid else { return }
Database.database().reference().child("following").child(userId).observe(.value) { (snapshot) in
self.user?.following = Int(snapshot.childrenCount)
}
}
The problem is that this takes very long to load and often freezes the entire app when you look at a user's profile. How can I speed this up or make it work more efficiently?
self.user?.following = Int(snapshot.childrenCount)
Is not an efficient solution. .childrenCount actually loops over the snapshot and counts all of the children which is going to be slow.
Instead you want to store the number of followers as a single value you can retrieve it faster.
following: {
uid: {
followingCount: 100,
follwersCount: 150
}
}
Then you can query like this:
Database.database().reference().child("following").child(userId).observeSingleEvent(of: .value) { (snapshot) in
if let counts = snap.value as? [String: AnyObject] }
let followingCount = counts["followingCount"] as? Int
let followersCount = counts["followersCount"] as? Int
// save these values somewhere
}
})
I would also recommend you increment / decrement the follower counts in a transaction block so the count doesn't get messed up. That can look something like this:
static func incrementCount(countName: String) {
if let uid = Auth.auth().currentUser?.uid {
let databaseReference = Database.database().reference()
databaseReference.child("following").child(uid).runTransactionBlock { (currentData: MutableData) -> TransactionResult in
if var data = currentData.value as? [String: Any] {
var count = data[countName] as! Int
count += 1
data[countName] = count
currentData.value = data
return TransactionResult.success(withValue: currentData)
}
return TransactionResult.success(withValue: currentData)
}
}
}
Lastly,
If you're going to use .observe you need to remove the reference. In this case though you aren't looking for updates so you can use .observeSingleEvent