Swift - Firebase - order collection - ios

I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX") inside my "Wishlists" - collection.
I tried this but that's not allowed:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").document(list.name).collection("wünsche").getDocuments()
This is my function:
func getWishes (){
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
var counter = 0
for list in self.dataSourceArray {
print(list.name) // -> right order
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments() { ( querySnapshot, error) in
print(list.name) // wrong order
if let error = error {
print(error.localizedDescription)
}else{
// DMAG - create a new Wish array
var wList: [Wish] = [Wish]()
for document in querySnapshot!.documents {
let documentData = document.data()
let wishName = documentData["name"]
wList.append(Wish(withWishName: wishName as! String, checked: false))
}
// DMAG - set the array of wishes to the userWishListData
self.dataSourceArray[counter].wishData = wList
counter += 1
}
}
}
}
This is what I actually would like to achieve in the end:
self.dataSourceArray[ListIDX].wishData = wList
Update
I also have a function that retrieves my wishlists in the right order. Maybe I can add getWishesin there so it is in the right order as well.
func retrieveUserDataFromDB() -> Void {
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
db.collection("users").document(userID).collection("wishlists").order(by: "listIDX").getDocuments() { ( querySnapshot, error) in
if let error = error {
print(error.localizedDescription)
}else {
// get all documents from "wishlists"-collection and save attributes
for document in querySnapshot!.documents {
let documentData = document.data()
let listName = documentData["name"]
let listImageIDX = documentData["imageIDX"]
// if-case for Main Wishlist
if listImageIDX as? Int == nil {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: UIImage(named: "iconRoundedImage")!, wishData: [Wish]()))
// set the drop down menu's options
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(UIImage(named: "iconRoundedImage")!)
}else {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: self.images[listImageIDX as! Int], wishData: [Wish]()))
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(self.images[listImageIDX as! Int])
}
// // create an empty wishlist
// wList = [Wish]()
// self.userWishListData.append(wList)
// reload collectionView and tableView
self.theCollectionView.reloadData()
self.dropDownButton.dropView.tableView.reloadData()
}
}
self.getWishes()
}
For a better understanding:
git repo

As #algrid says, there is no sense to order the collection using order() if you are going to get an specific element using list.name at the end, not the first or the last. I would suggest to change your code to:
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments()

I am trying to retrieve some documents but I need them to be ordered by some data ("ListIDX")
The following line of code will definitely help you achieve that:
db.collection("users").document(userID).collection("wishlists").order(by: "ListIDX").getDocuments() {/* ... */}
Adding another .document(list.name) call after .order(by: "ListIDX") is not allowed because this function returns a Firestore Query object and there is no way you can chain such a function since it does not exist in that class.
Furthermore, Firestore queries are shallow, meaning that they only get items from the collection that the query is run against. There is no way to get documents from a top-level collection and a sub-collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use the properties of documents in a single collection. So the most simple solution I can think of would be to use two different queries and merge the results client-side. The first one would be the above query which returns a list of "wishlists" and the second one would be a query that can help you get all wishes that exist within each wishlist object in wünsche subcollection.

I solved the problem. I added another attribute when saving a wish that tracks the index of the list it is being added to. Maybe not the smoothest way but it works. Thanks for all the help :)

Related

How to get progress of asynchronous Firebase data read?

I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!
let db = Firestore.firestore()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else{
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
}
You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.
The read starts when you call getDocuments.
The read is complete after the for loop in the getDocuments completion closure.
So:
let db = Firestore.firestore()
activityIndicatorView.startAnimating()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else {
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
}
}
As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.
You should also avoid force unwrapping.
struct PackageInfo {
let id: String
let name: String
let imageId: String
let radius: String
}
...
var packages:[PackageInfo] = []
...
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else if let documents = snapshot?.documents {
self.packages = documents.compactMap { doc in
if let title = doc.get("title") as? String,
let imageId = doc.get("imgID") as? String,
let radius = doc.get("radius") as? String {
return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
} else {
return nil
}
}
}
There is no progress reporting within a single read operation, either it's pending or it's completed.
If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.

SwiftUI: How to iterate over Documents in Cloud Firestore Collection and get Data?

I have this small project where a user can post an Image together with a quote, I would then like to display the Image and the quote togehter in their profile, as well as somewhere else so other users can see the post.
If I have this Cloud Firestore setup
where all of the Image Docs have the same 3 fields, but with different values.
How can I then iterate over all of the Image Docs and get the the Url and the quote? So I later can display the url together with the correct Quote?
And if this is for some reason not possible, is it then possible to get the number of Documents in a Collection?
BTW, I am not very experienced so I would appreciate a "kid friendly" answer if possible
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach({ (documentSnapshot) in
let documentData = documentSnapshot.data()
let quote = documentData["Quote"] as? String
let url = documentData["Url"] as? String
print("Quote: \(quote ?? "(unknown)")")
print("Url: \(url ?? "(unknown)")")
})
}
You can get all of the documents in a collection by calling getDocuments.
Inside that, snapshot will be an optional -- it'll return data if the query succeeds. You can see I upwrap snapshot and check for error in the guard statement.
Once you have the snapshot, you can iterate over the documents with documents.forEach. On each document, calling data() will get you a Dictionary of type [String:Any].
Then, you can ask for keys from the dictionary and try casting them to String.
You can wee that right now, I'm printing all the data to the console.
Keep in mind that getDocuments is an asynchronous function. That means that it runs and then returns at an unspecified time in the future. This means you can just return values out of this function and expect them to be available right after the calls. Instead, you'll have to rely on things like setting properties and maybe using callback functions or Combine to tell other parts of your program that this data has been received.
If this were in SwiftUI, you might do this by having a view model and then displaying the data that is fetched:
struct ImageModel {
var id = UUID()
var quote : String
var url: String
}
class ViewModel {
#Published var images : [ImageModel] = []
func fetchData() {
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
self.images = snapshot.documents.compactMap { documentSnapshot -> ImageModel? in
let documentData = documentSnapshot.data()
if let quote = documentData["Quote"] as? String, let url = documentData["Url"] as? String {
return ImageModel(quote: quote, url: url)
} else {
return nil
}
}
}
}
}
struct ContentView {
#ObservedObject var viewModel = ViewModel()
var body : some View {
VStack {
ForEach(viewModel.images, id: \.id) { item in
Text("URL: \(item.url)")
Text("Quote: \(item.quote)")
}
}.onAppear { viewModel.fetchData() }
}
}
Note: there are now fancier ways to get objects decoded out of Firestore using FirebaseFirestoreSwift and Combine, but that's a little outside the scope of this answer, which shows the basics

Reading data from Firestore in Swift

I am currently developing an IOS app, and I need a database! I've chosen the google firestore! I need to read some fields I create that have subfields!
Something like this:
db.collection("usersorders").document(uid).collection("order").addDocument(data: ["items":0, "order":["Book1":0,"Book2":0,"Book3":0]]){ (error) in
if error != nil {
// Show error message
print("Error saving user data")
}
}
Where I need to read the "Book1" value for example! I've looked in a lot of places, but I can't seem to find what I am looking for. Read subfields, from a field of a document!
#IBAction func AddtoCart(_ sender: Any) {
let uid = user!.uid
let docRef = db.collection("usersorders").document(user!.uid).collection("order").document()
docRef.getDocument(source: .cache) { (document, error) in
if let document = document {
let Book1 = document.get("Book1")
let Items = document.get("items")
let Book1now = Book1 as! Int + 1
let Itemsnow = Items as! Int + 1
}
}}
This is what I have been doing but it doesn't work! After writing the code to update the database with the Items/Book1 now values it just doesn't update! Please Help me
Given that your document data looks like this:
["items":0, "order":["Book1":0,"Book2":0,"Book3":0]]
You'll first need to access the order field in your document, before you can then find an item in that field
let order = document.get("order")
As far as I can see, this makes order a dictionary, so you can get the specific value from it with:
let book1 = order["Book1"] as Int

Assigning firestore data to custom object

I am trying to make an app that uses Firestore to store customer specific specs. I have a spec object that only has a few properties for now, but I need to take the data and assign it to a new Spec object, which then will be appended to an array to display on a tableView. I don't understand how to access the individual maps in the array to assign the values to each property for the spec. I currently have it set to print in the console, as every time I try to assign a value from the document it is nil. Essentially I need to store data under customers(a total list of all the customers) and among other data have an array of specs that contain specNumber, specDescription, and palletCount for each customer.
example:
Customer: Test
Specs:
3096:
Description: 50#top
pltCount: 250
3097:
Description: 50#bottom
pltCount: 250
Firestore data:
enter image description here
Code:
let settings = FirestoreSettings()
Firestore.firestore().settings = settings
db = Firestore.firestore()
db.collection("customers/test/specs")//.whereField("isCustomer", isEqualTo: true)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
Spec calls code:
struct Spec {
// Properties
var specNumber: String
var specDescription: String
var palletCount: Int
//var palletsOrdered = 0
init(specNum: Int, specDesc: String, pltCount: Int) {
specNumber = "\(specNum)"
specDescription = specDesc
palletCount = pltCount
}
}
You must separate access to documents from access to data within documents (you attempt to do it together). You cannot call getDocuments() on a document or a field within a document, only on a collection. So instead of db.collection("customers/test/specs").getDocuments(), try:
db.collection("customers").getDocuments() { (snapshot, error) in ... }
Then to get data from the documents:
db.collection("customers").getDocuments() { (snapshot, error) in
if let snapshot = snapshot { // lead by unwrapping the snapshot instead of the error
for doc in snapshot.documents { // iterate through the documents
guard let specs = doc.get("specs") as? [[String: Any]] else {
continue // continue loop
}
for s in specs { // iterate through the array of specs
if let specNum = s["SpecNum"] as? String,
let specDesc = s["SpecDesc"] as? String,
let pltCount = s["PalletCount"] as? Int {
let spec = Spec(specNum: specNum, specDesc: specDesc, pltCount: pltCount)
self.someArray.append(spec)
}
}
self.tableView.reloadData() // loop is done, reload
}
} else {
if let error = error {
print(error)
}
}
}
This is a very simplified version of how I imagine you'd actually want to implement it, depending on how the table/collection was reloaded (on the fly, routinely, or just once). Also, each document contains an array of specs but you're fetching all documents from the collection, which would give you a ton of specs without any indication of which spec is tied to which customer. But I suspect this is just early setup and you're just trying to get a handle on the API first.
Note: Maps in Firestore are called dictionaries in Swift and they always come back from Firestore as [String: Any] dictionaries. That's why when we originally unwrapped the specs map, we cast it as an array of dictionaries:
let specs = doc.get("specs") as? [[String: Any]]

Issue Reloading Table View After Adding Parse Object

I have a UITableViewController that displays data from a Parse query. It get the data and displays it fine except when I create a new object and run the query again to get the new data. When I create a new object the table view keeps the existing data in my array and displays it but it appends all the data from the query to the array so the objects that already existed prior to creating the new object get displayed twice. I tried emptying the arrays at the start of the query function but since I have the skip property set on the query I can't do that because my array will only get everything after the skip if the limit is reached. So, how can I just add the new object to my array?
I should also mention that I can't simply add the new object name to the array in addCollection() because I have to add the objectId to my objectID array.
func getCollections() {
activityIndicator?.startAnimating()
// collections = [] - Can't do this because of the skip (if the skip is used)
// objectID = []
let query = PFQuery(className: "Collections")
query.whereKey("user", equalTo: PFUser.currentUser()!)
query.orderByAscending("collectionName")
query.limit = limit
query.skip = skip
query.findObjectsInBackgroundWithBlock( {
(objects, error) -> Void in
if error == nil {
if let objects = objects as [PFObject]! {
for object in objects {
let collectionName = object["collectionName"] as! String
let id = object.objectId
self.collections.append(collectionName)
self.objectID.append(id!)
}
}
if objects!.count == self.limit {
self.skip += self.limit
self.getCollections()
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
self.activityIndicator!.stopAnimating()
}
} else {
var errorString = String()
if let message = error!.userInfo["error"] {
errorString = message as! String
}
print(errorString)
}
})
}
func addCollection(name: String) {
let collection = PFObject(className: "Collections")
collection["user"] = PFUser.currentUser()
collection["collectionName"] = name
collection.saveInBackground()
getCollections()
}
This code is logically flawed and can be simplified:
func addCollection(name: String) {
let collection = PFObject(className: "Collections")
collection["user"] = PFUser.currentUser()
collection["collectionName"] = name
collection.saveInBackground()
getCollections()
}
problems include:
your save runs in the background and isn't complete before you try to reload
your reload doesn't update or reset the skip and limit values
Unless you need to check for updates from other users then you shouldn't make a new request to the server to get new details. Instead you should add a completion block on the save and in there:
get the name and id and add those values to your data source arrays
update the skip value by adding one

Resources