Restart Firebase observe - swift

I created a method where I fetch all the published Books in Firebase. Each Book object stores a userId string value. I would like to fetch all books excluding currentUser books. I fetch 5 books every time I call the method starting from lastBookId, however if a user publishes more than 5 books, and are the first five, they can not be shown and as a result can not continue fetching them. I was thinking about increasing the limit value and calling the query observation again.
My code:
public func fetchBooksStarting(with lastBookId: String? = nil, completion: #escaping (Result<[Book], Error>) -> Void) {
var limit: UInt = 5
var books = [Book]()
let group = DispatchGroup()
var query = database.child("books").queryOrdered(byChild: "type")
if lastBookId != nil {
query = query.queryStarting(afterValue: BookType.Fiction.rawValue, childKey: lastBookId)
} else {
query = query.queryEqual(toValue: BookType.Fiction.rawValue)
}
query.queryLimited(toFirst: limit).observeSingleEvent(of: .value, with: { snap in
guard let snapshot = snap.children.allObjects as? [DataSnapshot] else {
completion(.failure(DatabaseErrors.failedToFetch))
return
}
books.removeAll()
for data in snapshot {
group.enter()
if let dict = data.value as? [String: AnyObject] {
let book = Book(dict: dict, bookId: data.key)
if book.userId == currentUserUid {
limit += 1
// recall query observe
} else {
books.append(book)
}
}
group.leave()
}
group.notify(queue: .main) {
completion(.success(books))
}
}, withCancel: nil)
}

Related

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 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

How can I detect firebase observe childAdded is stop calling when reach queryLimit?

Now, I'm so confused about firebase with observe using childAdded data event type. The reason why I use childAdded to observe my firebase because I want to make my list page dynamic whether firebase has new data insert.
And my question is how to know observe is stop calling when reach the queryLimit? Because I have a indicator and I want to turn it off when reach the queryLimit.
My firebase structure below:
root {
talkID1(id by auto create) {
attribute1 ...
attribute2 ...
attribute3 ...
time
}
talkID2(id by auto create){
...
time
}
... // many talk ID which auto create by firebase
}
As what I know, if using childAdd to observe, data will one child by child to passing data in call back. So If I have N datas in firebase and I think it will calls N<=5 times, right?
My completion handler below:
func receiveIfExist(completion: #escaping (_ data: (My data type) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: 5).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
})
}
I'm calling receiveIfExist this function in viewDidLoad().
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.receiveIfExist { (data) in
self.handleTalk(with: data) // do something with request data
self.myIndicator.stopAnimating() // WEIRD!!!! Please look comments below
/*
I think it can not be added here because this completion will call N<=5 times just what I said before.
I think it should detect what my queryLimit data is and check the request data is this queryLimit data or not.
If yes then stop indicator animating, if not then keep waiting the queryLimit reach.
*/
}
}
How can I detect the observe is reach queryLimit?
If I can detect then I can turn off my indicator when it reach.
Thank you!
queryLimited(toLast: 5)
means (in much simpler words) please get the last 5 values (order is decided by the previous part of your query)
1. Now, since you are sorting the data by times , the values with the last 5 times will be retrieved, therefore your observer will be triggered 5 times
2. Note that if you have less than 5 records say 2 records, then it will be triggered only twice because maximum limit is 5, not minimum limit
3. Another point is that say if a new child is added and when you sort the items again according to the time and the new child is one of the last 5 items then this observer will be triggered again.
so to get the query limit you can make some changes in your code like this:
func receiveIfExist(completion: #escaping (data: YourType, limitCount: Int) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: 5).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
self.index = self.index + 1
completion(data, self.index)
})
})
}
Then using the above function as follows:
var index = 0
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.receiveIfExist { (data, limitCount) in
self.handleTalk(with: data) // do something with request data
if limitCount == 5 {
self.myIndicator.stopAnimating()
}
}
}
UPDATED:
Since very good point raised by Kevin, that above solution will fail if we have say only two records and index will never be equal to 5 and myIndicator will not stop animating,
One solution that comes to my mind is this:
First we get the children count using observeSingleEvent:
func getChildrenCount(completion: #escaping (_ childrenCount: Int) -> Void){
Database.database.reference().child("root").observeSingleEvent(of:.value with: { (snapshot) in
completion(snapshot.children.count)
}
}
then we apply the query to get last 5 items:
func receiveIfExist(completion: #escaping (data: YourType, limitCount: Int) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: queryLimit).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
self.index = self.index + 1
completion(data, self.index)
})
})
}
then use this count in your code as follows:
var index = 0
var childrenCount = 0
var queryLimit = 5
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating() // start running indicator
self.getChildrenCount {(snapChildrenCount) in
self.childrenCount = snapChildrenCount
self.receiveIfExist { (data, limitCount) in
self.handleTalk(with: data) // do something with request data
if (self.childrenCount < self.queryLimit && limitCount == self.childrenCount) || limitCount == self.queryLimit {
DispatchQueue.main.async {
self.myIndicator.stopAnimating()
}
}
}
}
}
func receiveIfExist(limitCount: UInt, completion: #escaping (data: MyDataType) -> Void) {
let requestWithQuery = Database.database.reference().child("root").queryOrdered(byChild: "time")
requestWithQuery.queryLimited(toLast: limitCount).observe(.childAdded, with: { (snapshot) in
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
})
}
I also do this function for only observe single child's value
let requestTalks = Database.database.reference().child("root")
func getSingleTalk(by key: String = "", at limitType: TalkLimitType, completion: #escaping (_ eachData: MyDataType) -> Void) {
var requestSingleTalk: DatabaseQuery {
switch limitType {
case .first :
return self.requestTalks.queryOrdered(byChild: "time").queryLimited(toFirst: 1)
case .last :
return self.requestTalks.queryOrdered(byChild: "time").queryLimited(toLast: 1)
case .specificKey :
return self.requestTalks.child(key)
}
}
requestSingleTalk.observeSingleEvent(of: .value, with: { (snapshot) in
if limitType == .specificKey {
guard let value = snapshot.value as? [String: Any] else { return }
self.getSingleTalkInfo(key: snapshot.key, value: value, completion: { (data) in
completion(data)
})
} else {
guard let snapshotValue = snapshot.value as? NSDictionary,
let eachTalkKey = snapshotValue.allKeys[0] as? String,
let eachTalkValue = snapshotValue.value(forKey: eachTalkKey) as? [String: Any] else { return }
self.getSingleTalkInfo(key: eachTalkKey, value: eachTalkValue, completion: { (data) in
completion(data)
})
}
})
}
As a result, I can do something like this in my viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
self.myIndicator.startAnimating()
self.receiveIfExist(limitCount: 5) { (eachData) in
self.handleTalk(with: eachData)
self.getSingleTalk(at: .last, completion: { (lastData) in
if eachData.keyID == lastData.keyID{
DispatchQueue.main.async {
self.myIndicator.stopAnimating()
}
}
})
}
}

Swift 3: How to retry Firebase upload on failure

I'm initializing some data in Firebase that I will use to track a user's activity. I need to make sure this data is written to Firebase so I'm wondering what the best practice is for ensuring a critical upload was successful?
static func createUserActivityCounts(uid: String) {
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(5)) {
let databaseReference = Database.database().reference()
let userCounts: [String: Any] = ["posts": 0,
"comments": 0,
"likes": 0]
databaseReference.child("userActivity").child(uid).child("counts").setValue(userCounts) { (error, ref) -> Void in
if error != nil {
print(error!.localizedDescription)
}
}
}
}
I think the best way to resolve this issue is to check if the value exists whenever you attempt to query a critical upload. If the value isn't there then you initialize it. Here is the function I wrote to handle this.
private func getUserActivityCounts() {
if let uid = Auth.auth().currentUser?.uid {
userRef.child("userActivity").child(uid).child("counts").observeSingleEvent(of: .value, with: { snap in
if !snap.exists() {
// create user counts
if let uid = Auth.auth().currentUser?.uid {
DatabaseFunctions.createUserActivityCounts(uid: uid)
}
}
if let counts = snap.value as? [String: Any] {
if let numberOfLikes = counts["likes"] as? Int, let commentCount = counts["comments"] as? Int, let postCount = counts["posts"] as? Int {
DispatchQueue.main.async {
self.headerRef.postCount.text = String(postCount)
self.headerRef.commentCount.text = String(commentCount)
self.headerRef.likeCount.text = String(numberOfLikes)
self.collectionView.reloadData()
}
self.numberOfUserPosts = postCount
self.commentCount = commentCount
self.likeCount = numberOfLikes
}
}
})
}
}

Does Firebase know when data is no longer a part of query?

In my Firebase database, I have children containing Unix times, and I'm querying by times that occur after the current time. I'm putting the data of each child which matches that criteria into a UICollectionView. When the current time surpasses the time of one of the children, I want for the child to expire, and to get removed from the UICollectionView. Currently, it isn't getting removed until I restart the app. Here is some of the code:
// in viewDidLoad
self.events_query = Database.database().reference().child("events").queryOrdered(byChild: "end-time").queryStarting(atValue: Date().timeIntervalSince1970)
// in viewWillAppear
func observeAllEvents() {
self.events_query.observe(.value, with: { (snapshot) in
guard let eids_dict = snapshot.value as? [String : AnyObject] else { return }
let eids = Array(eids_dict.keys)
for eid in eids {
print(eid)
}
Event.getAllEvents(with: eids, ref: self.events_query.ref, completion: { (events) in
if let feed_datasource = self.datasource as? EventFeedDatasource {
feed_datasource.events = events
}
DispatchQueue.main.async {
self.collectionView?.reloadData()
}
})
})
}
// in viewDidDisappear
self.events_query.removeAllObservers()
Here's the function getAllEvents:
static func getAllEvents(with eids: [String], ref: DatabaseReference, completion: #escaping (_ events: [Event]) -> Void) {
var events = [Event]()
let dispatch_groups = [DispatchGroup(), DispatchGroup()]
for eid in eids {
dispatch_groups[0].enter()
ref.child(eid).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String : AnyObject] else { return }
dispatch_groups[1].enter()
// I'm not including `load` because all it does is parse the snapshot
Event.load(with: dictionary, completion: { (event) in
events.append(event)
dispatch_groups[1].leave()
})
dispatch_groups[0].leave()
})
}
dispatch_groups[0].notify(queue: DispatchQueue.main) {
dispatch_groups[1].notify(queue: DispatchQueue.main) {
completion(events)
}
}
}