Firestore iOS - Ordering collection by field in document - ios

I have an array called "homeList" which observers "CURRENT_USER_FRIENDS_REF" collection and places it in the array. How can I make it so I can order this array by the "timestamp" field found in the document snapshot.
homeList array function
var homeList = [User]()
func addHomeObserver(_ update: #escaping () -> Void) {
CURRENT_USER_FRIENDS_REF.getDocuments { snapshot, error in
self.homeList.removeAll()
guard error == nil else {
#if DEBUG
print("Error retrieving collection")
#endif
return
}
let group = DispatchGroup()
for document in snapshot!.documents {
let whosfrom = document.get("fromId") as? String
let id = document.documentID
**let timestamp = document.get("timestamp") as? NSNumber**
group.enter()
self.getUser(id, completion: { (user) in
if whosfrom != self.CURRENT_USER_ID {
self.homeList.append(user)
}
group.leave()
})
}
group.notify(queue: .main) {
update()
}
}
}
Current user friends reference:
var CURRENT_USER_FRIENDS_REF: CollectionReference {
return CURRENT_USER_REF.collection("friends")
}
Thanks.

You can use order(by on a collection reference to get the result.
CURRENT_USER_FRIENDS_REF.order(by: "timestamp", descending: true).getDocuments { snapshot, error in
}

Related

How can we paginate a firebase / firestore query on ios swift?

I try to find a solution for paginate a firebase query on ios/swift but I couldn't build algorithm for my state.
My method is like this:
func downloadData(completion: #escaping ([Post]) -> Void) {
// download data with pagination
let firestoreDatabase = Firestore.firestore()
var first = firestoreDatabase.collection("posts").order(by: "date", descending: true).limit(to: 5)
first.addSnapshotListener{ snapshot, error in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.postList.removeAll(keepingCapacity: false)
DispatchQueue.global().async {
for document in snapshot.documents {
// getting data from document stuff ...
self.postList.append(self.post)
}
completion(self.postList)
}
// how can I repeat this query as long as lastSnapshot exist
firestoreDatabase.collection("posts").order(by: "date", descending: true).start(afterDocument: lastSnapshot).addSnapshotListener { querySnapshot, error in
}
}
}
I tried following mindset but it didn't work, and entered an infinite loop. I didn't understand why it is.
func downloadData(completion: #escaping ([Post]) -> Void) {
// download data with pagination
let firestoreDatabase = Firestore.firestore()
var first = firestoreDatabase.collection("posts").order(by: "date", descending: true).limit(to: 5)
first.addSnapshotListener{ snapshot, error in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.postList.removeAll(keepingCapacity: false)
DispatchQueue.global().async {
for document in snapshot.documents {
// geting data from document stuff ...
self.postList.append(self.post)
}
completion(self.postList)
}
repeat {
firestoreDatabase.collection("posts").order(by: "date", descending: true).start(afterDocument: lastSnapshot).addSnapshotListener { querySnapshot, error in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.postList.removeAll(keepingCapacity: false)
DispatchQueue.global().async {
for document in snapshot.documents {
// getting data from document stuff ...
self.postList.append(self.post)
}
completion(self.postList)
}
lastSnapshot = snapshot.documents.last
}
} while(lastSnapshot.exists)
}
}
I think lastSnapshot must be nil after the query loop but it is appear that it is still exist.
how can I fix lastSnapshot problem? Or is there different mindset / easiest way to paginate?
In firebase documents, it says just use this but how can we repeat query that has " .start(afterDocument: lastSnapshot) " stuff?
First and foremost, for plain-vanilla pagination, don't use a snapshot listener when fetching documents. You can paginate documents with a snapshot listener but the process is more complex.
I've embedded my notes into the comments in the code below for clarity.
let pageSize = 5
var cursor: DocumentSnapshot?
func getFirstPage(completion: #escaping (_ posts: [Post]?) -> Void) {
let db = Firestore.firestore()
let firstPage = db.collection("posts").order(by: "date", descending: true).limit(to: pageSize)
firstPage.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
// Don't leave the caller hanging on errors; return nil,
// return a Result, throw an error, do something.
completion(nil)
if let error = error {
print(error)
}
return
}
guard !snapshot.isEmpty else {
// There are no results and so there can be no more
// results to paginate; nil the cursor.
cursor = nil
// And don't leave the caller hanging, even on no
// results; return an empty array.
completion([])
return
}
// Before parsing the snapshot, manage the cursor.
if snapshot.count < pageSize {
// This snapshot is smaller than a page size and so
// there can be no more results to paginate; nil
// the cursor.
cursor = nil
} else {
// This snapshot is a full page size and so there
// could potentially be more results to paginate;
// set the cursor.
cursor = snapshot.documents.last
}
var posts: [Post] = []
for doc in snapshot.documents {
posts.append(newPost) // pseudo code
}
completion(posts)
}
}
func continuePages(completion: #escaping (_ posts: [Post]?) -> Void) {
guard let cursor = cursor else {
return
}
let db = Firestore.firestore()
let nextPage = db.collection("posts").order(by: "date", descending: true).limit(to: pageSize).start(afterDocument: cursor)
nextPage.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
completion(nil)
if let error = error {
print(error)
}
return
}
guard !snapshot.isEmpty else {
// There are no results and so there can be no more
// results to paginate; nil the cursor.
cursor = nil
completion([])
return
}
// Before parsing the snapshot, manage the cursor.
if snapshot.count < pageSize {
// This snapshot is smaller than a page size and so
// there can be no more results to paginate; nil
// the cursor.
cursor = nil
} else {
// This snapshot is a full page size and so there
// could potentially be more results to paginate;
// set the cursor.
cursor = snapshot.documents.last
}
var morePosts: [Post] = []
for doc in snapshot.documents {
morePosts.append(newPost) // pseudo code
}
completion(morePosts)
}
}

Firebase for SWIFT iOS: returning nil value in function [duplicate]

This question already has an answer here:
Why function return nil FireBase Swift [duplicate]
(1 answer)
Closed 2 years ago.
I am trying to access my firestore database which store the userid, username in the collection called user, I want to return the username of the current user, however when I try to run the below function, it is returning me nil value.... do you know anything that I'm missing? A completion handler?
func display(userid: String) -> String
{
var displayname: String
let docRef = db.collection("user").document(uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
displayname = document.get("username") as! String
} else {
print("Document does not exist")
}
}
return displayname
}
Then I tried to change to this but it still doesn't change my variable. It seems like it's trapped inside my function
var name = "placeholder"
func display(userid: String, completion: #escaping (String) -> Void) {
let docRef = db.collection("user").document(userid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
if let displayName = document.get("username") as? String {
self.name = displayName
completion(displayName)
}
} else {
print("Document does not exist")
}
}
}
func create()
{
display(userid: uid){
[weak self] displayName in
print(displayName)
self!.name = displayName
}
var ref: DocumentReference? = nil
ref = db.collection("Request").addDocument(data: ["requestername": "name"]
.....
}
You need to use closures as getDocument() does not return synchronously.
func display(userid: String, handler: (String) -> Void) {
let docRef = db.collection("user").document(uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
if let displayName = document.get("username") as? String {
handler(displayName)
}
} else {
print("Document does not exist")
}
}
}
And use it like so:
self.myLabel.text = "Loading..."
display(userid: {USER_ID}) { [weak self] displayName in
print(displayName)
self.myLabel.text = displayName
}

Optional Still Returning Nil After Assigning Value

I am working on a similar feature to 'liking/unliking a post'.
I have an MVVM architecture as;
struct MyStructModel {
var isLiked: Bool? = false
}
class MyStructView {
var isLiked: Bool
init(myStructModel: MyStructModel) {
self.isLiked = myStructModel.isLiked ?? false
}
}
I successfully get the value of whether the post is liked or not here;
func isPostLiked(documentID: String, completion: #escaping (Bool) -> Void) {
guard let authID = auth.id else { return }
let query = reference(to: .users).document(authID).collection("liked").document(documentID)
query.getDocument { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let data = snapshot?.data() else { return }
if let value = data["isLiked"] as? Bool {
completion(value)
} else {
completion(false)
}
}
}
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let snapshotDocuments = snapshot?.documents else { return }
for document in snapshotDocuments {
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
// isLiked is nil here...
self.isPostLiked(documentID: post.documentID!) { (isLiked) in
post.isLiked = isLiked
print("MODEL SAYS: \(post.isLiked!)")
// isLiked is correct value here...
}
posts.append(post)
}
completion(posts)
}
}
}
However, when it gets to my cell the value is still nil.
Adding Cell Code:
var post: MyStructView? {
didSet {
guard let post = post else { return }
print(post.isLiked!)
}
}
Your isLiked property is likely nil in your cells because the retrieveReviews function doesn't wait for the isPostLiked function to complete before completing itself.
You could easily solve this issue by using DispatchGroups. This would allow you to make sure all of your Posts have their isLiked value properly set before being inserted in the array, and then simply use the DispatchGroup's notify block to return all the loaded posts via the completion handler:
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { [weak self] (snapshot, error) in
guard let self = self else { return }
if error != nil {
return
}
guard let documents = snapshot?.documents else { return }
let dispatchGroup = DispatchGroup()
for document in documents {
dispatchGroup.enter()
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
self.isPostLiked(documentID: post.documentID!) { isLiked in
post.isLiked = isLiked
posts.append(post)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(posts)
}
}
}

How to create array in array using collections and sub collections in Firestore?

I need to create an array of Categories that contains Questions array.
struct CategoryFB {
var title: String
var id: Int
var questions: [QuestionsFB]
var dictionary: [String : Any] {
return ["title" : title,
"id" : id]
}
}
extension CategoryFB {
init?(dictionary: [String : Any], questions: [QuestionsFB]) {
guard let title = dictionary["title"] as? String, let id = dictionary["id"] as? Int else { return nil }
self.init(title: title, id: id, questions: questions)
}
}
Firestore has a following structure
Collection("Categories")
Document("some_id")
Collection("Questions")
How to create array like this?
array = [Category(title: "First",
questions: [
Question("1"),
...
]),
... ]
My try was wrong:
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data(), questions: questionsArray!)})
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Your main problem seems to stem from the fact that you're regenerating your categories array every time you run your subquery, and when you do, you're only supplying a single questions array to the entire thing.
There's lots of ways to fix this. I would probably break it up so that you a) First allow yourself to create a category array without any questions, and then b) Go back through each of your individual subQueries and insert them into your categories as you get them.
Your final code might look something like this. Note that this would mean changing your Category object so that you can first create it without a Questions array, and implementing this custom addQuestions:toCategory: method (which would be a whole lot easier if you stored your categories as a dictionary instead of an array)
db.collection("Categories").order(by: "id", descending: false).getDocuments {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
self.categoriesArray = querySnapshot?.documents.compactMap({CategoryFB(dictionary: $0.data()})
for document in querySnapshot!.documents {
print(document.documentID)
self.db.collection("Categories").document(document.documentID).collection("Questions").getDocuments(completion: { (subQuerySnapshot, error) in
if error != nil {
print(error!.localizedDescription)
} else {
var questionsArray: [QuestionsFB]?
questionsArray = subQuerySnapshot?.documents.compactMap({QuestionsFB(dictionary: $0.data())})
self.addQuestions(questionsArray toCategory: document.documentID)
print(self.categoriesArray![0].questions.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
})
}
}
}
Alternately, if you think you're going to be in a situation where you're always going to want to grab your questions every time you want to grab a category, you might consider not putting them in a subcollection at all, and just making them a map in the original category document.
This is the solution which I found by myself. Hopefully this will help someone in the future.
func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = db.collection("Categories")
var data = [Any]()
rootCollection.order(by: "id", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
for category in topSnapshot {
rootCollection.document(category.documentID).collection("Questions").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var questions = [Question]()
for document in snapshot {
let title = document.data()["title"] as! String
let details = document.data()["details"] as! String
let article = document.data()["article"] as! String
let link = document.data()["link"] as! String
let id = document.data()["id"] as! String
let possibleAnswers = document.data()["possibleAnswers"] as! [String]
let rightAnswerID = document.data()["rightAnswerID"] as! Int
let newQuestion = Question(title: title, article: article, details: details, link: link, possibleAnswers: possibleAnswers, rightAnswerID: rightAnswerID, id: id)
questions.append(newQuestion)
}
let categoryTitle = category.data()["title"] as! String
let collectionID = category.data()["id"] as! Int
let newCategory = Category(title: categoryTitle, id: collectionID, questions: questions)
data.append(newCategory)
//Return data on completion
completion(data)
})
}
}
})
}

Proper way to use Firestore diff

I'm Calling this function to retrieve users from firestore:
Each time a user is modify I want to update the users array.
func fetchUsers( complete: #escaping ( _ success: Bool, _ users: [User], _ error: Error? )->()) {
//self.users = []
let circleId = UserDefaults.standard.string(forKey: "circleId") ?? ""
DataService.call.REF_CIRCLES.document(circleId).collection("insiders").order(by: "position", descending: false).addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let data = diff.document.data()
let id = diff.document.documentID
let user = User(key: id, data: data)
self.users.append(user)
complete(true, self.users, nil)
}
if (diff.type == .modified) {
// Update users array if user data is modified
if !self.users.isEmpty {
self.users = []
let data = diff.document.data()
let id = diff.document.documentID
let user = User(key: id, data: data)
self.users.append(user)
complete(true, self.users, nil)
}
}
if (diff.type == .removed) {
print("Removed user: \(diff.document.data())")
}
}
}
}
However my array always return 1 if there's only one user modified and my collectionview reload and then only show one user! How do I
return all the users even if only one was modified?
Thanks
New Function:
func fetchUsers( complete: #escaping ( _ success: Bool, _ users: [User], _ error: Error? )->()) {
self.users = []
let circleId = UserDefaults.standard.string(forKey: "circleId") ?? ""
DataService.call.REF_CIRCLES.document(circleId).collection("insiders").order(by: "position", descending: false).addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documents.forEach { diff in
let data = diff.data()
let id = diff.documentID
let user = User(key: id, data: data)
self.users.append(user)
complete(true, self.users, nil)
}
}
}
Now I'm seeing a lot of duplicate users in my collectionview
You're looping over QuerySnapshot.documentChanges, which only contains documents that changed since the last snapshot.
To get all documents in the query (instead of just the modified ones), loop over QuerySnapshot.documents instead:
DataService.call.REF_CIRCLES.document(circleId).collection("insiders").order(by: "position", descending: false).addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documents.forEach { diff in
...

Resources