Parse Large Firebase Snapshot - swift

I am downloading a large file structure from Firebase. However when printing the keys and values one of the subfolders is showing as <__NSArrayM> I tried to cast it to [String:Any] but it is crashing. Here is my code:
DATABASE.mainOrder.child("incomplete").observe(DataEventType.value, with: { (snapshot) in
let array = snapshot.value as! [String:[String:Any]]
for (_, value) in array {
for number in value {
if "\(number.key)" == "itemsInOrder" {
let items = number.value as! [String:Any]
//let items = Array(arrayLiteral: number.value)
print("itemsInOrder: \(items)")
} else {
print("\(number.key): \(number.value)")
}
}
}
})
However it is crashing and giving me this error:
Could not cast value of type '__NSArrayM' (0x2037d2608) to 'NSDictionary' (0x2037d21d0).
This is the code that I am trying to get:
itemsInOrder: [<__NSArrayM 0x281c060a0>({
amount = "2";
length = "5";
height = "7";
width = "10";
})]
Below is the Firebase json export
{
"TH000" : {
"docUUIDStatus" : "Sent",
"expectedShip" : "May 12, 2021",
"itemsInOrder" : [ {
"amountOrdered" : "2000 sq ft",
"bevel" : "None",
"floorLength" : "2-8",
"floorWidth" : "4",
"specialOrder" : "None",
"specialOrderLength" : "",
"status" : "completed"
} ],
"orderDate" : "May 11, 2021",
"orderNumber" : "TH000",
"orderNumberLowercased" : "th000",
"orderTime" : "2:30:30 PM",
"orderType" : "Order",
"purchaseOrderNumber" : "TH10051",
"purchaseOrderNumberLowercased" : "th10051",
"status" : "completed"
}
}

What you have in itemsInOrder is an array of dictionaries, so to get the data from it, you either have to loop over number in a nested loop, or extract it with:
let items = number.value as! [[String:Any]]
Since I find the double [[ a bit hard to see at first glance, I actually prefer this equivalent syntax:
let items = number.value as! [Dictionary<String:Any>]
Unrelated to the actual problem: your data structure is a bit of an antipattern in Firebase, as reading any order also now means that you end up loading all items in that order. This becomes wasteful when you want to show a list of order names.
The more idiomatic data structure it so have two top-level nodes:
orders
orderItems
In the first you store the properties for each order, such as orderType and status, under the order ID. In the second node you store the list of order times, also under the order ID.
With that structure, your current query will require two steps:
A query to determine the IDs of the orders with the requested status.
Then an additional load for each order to get the items for that order.
This second step is not nearly as slow as you may think, as Firebase pipelines the requests over a single existing connection.

The answer provided by Frank is spot on. I would like to suggest another approach.
We've found that DataSnapshots are super flexible and easy to work with - keeping data as a DataSnapshot for a long as possible provides easier access, especially when the structure is a bit deep. When casting to array's and dictionaries you end up with this kind of thing
[String: [String: [String: Any]]]
which is really cumbersome to work with and troubleshoot.
So here's the code that will read in the data as specified in the question. Note we never convert anything to a Dictionary and use arrays to guarantee ordering will be maintained from the retrieved DataSnapshots.
func readData() {
let ref = self.ref.child("main_order").child("incomplete") //self.ref points to my firebase
ref.observe(.value, with: { snapshot in //first node will be TH000
let childSnapArray = snapshot.children.allObjects as! [DataSnapshot]
for childSnap in childSnapArray {
print("parent node: \(childSnap.key)")
let status = childSnap.childSnapshot(forPath: "docUUIDStatus").value as? String ?? "No Status"
print(" status: \(status)")
let itemsInOrder = childSnap.childSnapshot(forPath: "itemsInOrder") as DataSnapshot
let itemSnapArray = itemsInOrder.children.allObjects as! [DataSnapshot]
print(" \(itemsInOrder.key)")
for itemChildSnap in itemSnapArray {
let amount = itemChildSnap.childSnapshot(forPath: "amountOrdered").value as? String ?? "No amount"
let length = itemChildSnap.childSnapshot(forPath: "floorLength").value as? String ?? "No length"
let width = itemChildSnap.childSnapshot(forPath: "floorWidth").value as? String ?? "No width"
let status = itemChildSnap.childSnapshot(forPath: "status").value as? String ?? "No status"
print(" ", amount, length, width, status)
}
}
})
}
I dropped in a few print statements along the way to show the code execution sequence. The output to console is this
parent node: TH000
status: Sent
itemsInOrder
2000 sq ft 2-8 4 completed
The one other thing is the array stored within itemsInOrder. It's not clear why that's an array and in general Arrays should be avoided in The Realtime Database if possible. There are usually better ways to structure the data. Arrays cannot be directly altered - if you want to insert or delete an element the entire array must be read in, modified and written back out.
So the above code may need to be slightly modified to handle the itemsInOrder node if there are multiple array elements, for example.

Related

Instead of running a snapshot for all users, how do you set up multiple queries to limit the number of users sent to the device?

What I have: A snapshot of all users with a bunch of if statements that eventually returns an array of users that get displayed.
What I need: The array of end users to be used in a .query in the line preceding the snapshot.
Why do I need this: This line is so that the entire database of users is not run on the client.
More specifically, what do I need to query for: A) Users who have a child "caption"(timestamp) with a timestamp that is in today, AND, B) who are 3000 miles from the current user.
JSON of DB
"people" : {
"02PdiNpmW3MMyJt3qPuRyTpHLaw2" : {
"Coordinates" : {
"latitude" : -25.809620667034363,
"longitude" : 28.321706241781342
},
"PhotoPosts" : "https://firebasestorage.googleapis.com/v0/b/daylike-2f938.appspot.com/o/images%2F02PdiNpmW3MMyJt3qPuRyTpHLaw2%2FPhotoPosts?alt=media&token=24fee778-bcda-44e3-aa26-d7c2f8509740",
"caption" : 1602596281762, /// this is timestamp
"postID" : "02PdiNpmW3MMyJt3qPuRyTpHLaw2"
},
"e1" : “cvvvv666",
"e2" : "aol.com",
" "postID" : "0RnqWV7Gd9Z0bUW9nUvizMQOjK73",
"users" : "cvvvv666#aol.com"
},
.
var dict = CLLocation()
...
dict = CLLocation(latitude: lat, longitude: lon)
...
let thisUsersUid = Auth.auth().currentUser?.uid
//this line below is where the refArtists2 query should go. in other words send all users to device that meet the 2 if statements, which is represented by self.people.append(peopl)//
let refArtists2 = Database.database().reference().child("people").queryOrdered(byChild: "caption").queryEqual(toValue: ANY Timestamp in today).queryOrdered(byChild:Coordinates). queryEqual(toValue:ThoseCoordinates which make the distance to current user less than 3000 miles)
refArtists2.observe(DataEventType.value, with: { snapshot in
if snapshot.childrenCount>0{
self.people.removeAll()
for people in snapshot.children.allObjects as! [DataSnapshot] {
if people.key != thisUsersUid {
let peopleObject = people.value as? [String: AnyObject]
let peopleCoordinates = peopleObject?["Coordinates"] as? String
let peoplecaption = peopleObject?["caption"] as? Int //is timestamp
let peoplepostID = peopleObject?["postID"] as? String
let coordSnap = people.childSnapshot(forPath: "Coordinates")
guard let lat = coordSnap.childSnapshot(forPath: "latitude").value as? CLLocationDegrees else { return }
guard let lon = coordSnap.childSnapshot(forPath: "longitude").value as? CLLocationDegrees else { return }
let locCoord = CLLocation(latitude: lat, longitude: lon)
let coordSnap12 = people.childSnapshot(forPath: "caption").value as? Int ?? 0
let date = Date(timeIntervalSince1970: TimeInterval(coordSnap12)/1000.0)
//let secondsInDay = 86400
**if Calendar.current.isDateInToday(date)** {
let distance = locCoord.distance(from: self.dict)
print(distance, "distancexy")
**if distance/1609.344 < 3000**{
let peopl = Userx(Coordinates: peopleCoordinates, distance:distance, caption: peoplecaption, postID: peoplepostID)
self.people.append(peopl)
let d = people.key as! String
self.printPersonInfo(uid:d) ///////This is used to reload the data
} else {
print ("w")
}
} else {
print ("alphaaa")
}
}
print("aaaaaaaa", self.people.map {$0.distance})
}
self.people.sort { ($0.distance ?? 0) < ($1.distance ?? 0) } ////////This sorting with distance is used with returning the cell. people is used as uid array to return the cell.
}
})
} else {
print("no")
}
})
Ancillary caveat: the self.people.sort { ($0.distance ?? 0) < ($1.distance ?? 0) }sorting is important, so the queries should not impede that. I am a bit concerned with using queryOrdered in that it orders the array of users in the wrong order. If it does, a C) query should be: The order of the users must be with the closest users to the logged in user first. The furthest from the logged in user must go last in the array.
Another way of asking this would be: Instead of running a snapshot of all users, how do you query the snapshot's 'end result sort' when making the snapshot?
The timestamp is seconds since 1970
My attempt at the date query below. I took the code and tried to put the code that gets the date before the actual query(currently the code that gets the date is after the snapshot of all users).
var ppp: String! ////this should be the uid of all users in db
let people = Database.database().reference().child("people").child(self.ppp).child("captions")
people.observe(DataEventType.value, with: { snapshot in
let captionss = snapshot.value as? Int ?? 0
let date = Date(timeIntervalSince1970: TimeInterval(captionss)/1000.0)
let query1 = Database.database().reference().child("people").queryOrdered(byChild: "caption").where?(isDateInToday(date))
Edit: This answer is in Firestore, not Realtime Database. However, the concepts are the same.
The question is several questions in one; asking about distance, compound queries and how to query Firebase in general. I will post this answer to address the second two and distance queries are addressed in the comment to the question.
Once the query pattern is understood, they become easier and most importantly; it becomes more obvious that how your data is structured depends on what queries you want to run against that data.
Suppose we have a users collection with user documents - each documentId is the users uid
users
uid_0
name: "Leroy"
and then we have the posts for the users - each post contains some text, a timestamp of the post, the uid of the user that posted it, what the topic is and a url of a picture that appears in the post. Notice I am storing posts in a separate collection; why read in a bunch of user data when we only want to know about their post.
posts
post_id
postText: "pretty flowers"
postDate: "20201103"
postUrl: "www....."
postUid: "uid_0"
postTopic: "flowers"
Let suppose we want to get posts from today that are about flowers, and then also get the posters name and output who posted the message and what they said.
To do this we will need a compound query and then a subquery to retrieve the posters name as well.
func getTodaysPostsAboutFlowers() {
let postsCollection = self.db.collection("posts")
let query = postsCollection.whereField("postDate", isEqualTo: "20201103").whereField("postTopic", isEqualTo: "flowers")
query.getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
for doc in docs {
let postText = doc.get("postText") as? String ?? "No text"
guard let postersUid = doc.get("postUid") as? String else { return }
self.outputPostTextAndUserName(withText: postText, andUid: postersUid)
}
})
}
The above performs a compound query on both the postDate field as the postTopic field.
The above then calls another function to retrieve the users name and output both the name and what they said
func outputPostTextAndUserName(withText: String, andUid: String) {
let usersCollection = self.db.collection("users")
let theUserDoc = usersCollection.document(andUid)
theUserDoc.getDocument(completion: { documentSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = documentSnapshot {
let postersName = doc.get("name") as? String ?? "No Name"
print("\(postersName) posted: \(withText)")
}
})
}
and the output
Leroy posted: pretty flowers
As you can see, there's no need to load all of the users, no need to iterate over results etc. Even if you have a billion users, this will only return a subset of that data which is a best practice when working with huge data sets; only get the data you're interested in.
Edit. The OP is asking about querying for nodes containing today. The simple solution is to have one child node containing a timestamp which would contains specific date data and then another child node just containing today data in YYYYMMDD format.
people
uid_x
timetamps: 9023490823498 //Date(timeIntervalSince1970:
todaystamp: "20201106" // yyyymmdd format
that makes querying for nodes that contain today very simple.

Accessing Firebase Data inside unique AutoID

This is my first question and I'm still learning Swift/Xcode/Firebase, so I appreciate your patience. I've been stalking StackOverflow and have found a lot of answers to help with various things, but nothing that makes sense for the problem I've been struggling with for 2 days.
I am writing a program that will save a date picked on a previous viewcontroller and a set of user-entered floats from text fields to a Firebase database, and append each data set as a separate entry instead of overwriting the previous data. Using the first block of code below, I've got this problem solved except I can't find a way to do it without using AutoID. This leaves me with a setup like this in Firebase, but with multiple categories and "optionSelected" sections in each category:
program-name
Category 1
optionSelected
L1cggMnqFqaJf1a7UOv
Date: "21-12-2017"
Variable 1 Float: "12345"
Variable 2 Float: "26.51"
L1ciVpLq1yXm5khimQC
Date: "30-12-2017"
Variable 1 Float: "23456"
Variable 2 Float: "35.88"
Code used to save:
func newWithNewVars() {
let myDatabase = Database.database().reference().child("Category 1").child(optionSelected)
let variable1 = textField1.text
let variable2 = textField2.text
let variable1Float = (textField1.text! as NSString).floatValue
let variable2Float = (textField2.text! as NSString).floatValue
let writeArray = ["Date": textPassedOverDate, "Variable 1 Float": variable1Float, "Variable 2 Float": variable2Float]
myDatabase.childByAutoId().setValue(gasArray) {
(error, reference) in
if error != nil {
print(error!)
}
else {
print("Message saved successfully!")
}
}
}
The problem comes with recalling data. Since the AutoID is unique, I can't figure out how to access the data deeper inside for calculations. Specifically, I want to be able to make a new entry, press the save data button, and have it find the most recent entry in the "optionSelected" section so it can do calculations like subtract the older variable 1 from the new variable 1 and such.
Given the above description, layout, and code used above, what code structure would allow me to find the most recent date and access the data inside the AutoID sections for a specific category and "optionSelected"?
Thank you for your help.
The issue you're having is that you're trying to dig deeper but can't as you don't have a hold of that id. You'll want to use the .childAdded in your reference observation when you want to get inside of a list in your JSON tree when you don't have a hold of that id to get inside - this will be called as many times as there are values inside of Category 1 tree:
let reference = Database.database().reference()
reference.child("Category 1").child("optionSelected").observe(.childAdded, with: { (snapshot) in
let uniqueKey = snapshot.key // IF YOU WANT ACCESS TO THAT UNIQUE ID
print(uniqueKey)
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
let date = dictionary["date"] as? String
let variableOne = dictionary["Variable 1 Float"] as? Float
let variableOne = dictionary["Variable 2 Float"] as? Float
}, withCancel: nil)
You may also want to avoid using spaces in your database keys to avoid any problems in the near future. I'd stick with the common lowercased underscore practice e.g. "category_1" or "variable_2_float"

swift firebase nested children count

groups
--group1(autoid)
---subgroup1(autoid)
----id
----ownerId
----description
--group2(autoid)
---subgroup2(autoid)
----id
----ownerId
----description
In a structure like over here i have to count all occurrencies of ownerId that are equal to my id (currentUserId) in all groups, can somebody help me?
what i've done so far:
root.child("groups").observe(.value, with: {(snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
var count = 0
for child in result {
let orderID = child.key as String //get autoID
self.root.child("groups/\(orderID)/").queryOrdered(byChild: "ownerId").queryEqual(toValue: self.currentUserId).observe(.value, with: { (snapshot: DataSnapshot!) in
print(snapshot.childrenCount, "quanti sono")
count += (Int(snapshot.childrenCount))
print(count)
})
}
}
})
with this i can get a count but it updates all cycles... i need i need the final value outside
One important aspect of Firebase Structures is denormalizing or flattening the structure. Denormalized data generally makes queries much easier and while conceptually the structure you are using works for some tasks, it makes doing the query you want challenging.
So, I would suggest an alternate structure that would make the query super simple, and not loose other functionality.
A change to the structure like this:
groups
group1: true
group2: true
subgroups
subgroup1(autoid)
id
ownerId
description
belongs_to_group: "group1"
subgroup2(autoid)
id
ownerId
description
belongs_to_group: "group2"
Then if you want to count all of subgroups with a particular ownerId
let subGroupsRef = self.ref.child("subgroups")
let query = subGroupsRef.queryOrdered(byChild: "ownerId").queryEqual(toValue: "their id")
query.observeSingleEvent(of: .value) { snapshot in
let count = snapshot.childrenCount
print(count)
}
Edit:
Based on the comment, here's an way to get the count based on your current structure. It's pretty brute force and the code could be reduced considerably but I left it verbose for readability
let groupsRef = self.ref.child("groups")
groupsRef.observeSingleEvent(of: .value, with: { snapshot in
var count = 0
for groupChild in snapshot.children {
let groupSnap = groupChild as! DataSnapshot
for subGroupChild in groupSnap.children {
let subGroupSnap = subGroupChild as! DataSnapshot
let dict = subGroupSnap.value as! [String: Any]
let uid = dict["owner_id"] as! String
if uid == "uid_0" {
count += 1
print(count)
}
}
}
print("total found \(count)")
})
Where this fails is if you have a lot of nodes as they are all initially loaded in (by .value) so it could be iterated over in code. If it's a few thousand it works well and is very fast.
You can achieve this step by doing a single observe of .childAdded type. The .childAdded on an sub child, in your example groups, is like a for loop that iterate all nodes.
With this configuration you can append a .queryOrdered(byChild:) and a .queryEqual(toValue:):
ref.child("groups")
.queryOrdered(byChild: "ownerID")
.queryEqual(toValue: 123)
.observe(.childAdded)
{ (snap) in
print(snap)
}
After that, if you want to count all this child, you need to add a property on your class
This is a test example:
To optimize performance remember to add a .indexOn rule on your firebase app:
"groups" : {
".indexOn" : "ownerID"
}
Hope this help you ;)

How to pass and get multiple URLQueryItems in Swift?

Ok, I am working in an iMessage app and am trying to parse more than 1 url query item from the selected message here- I have been successful getting/sending just 1 value in a query:
override func willBecomeActive(with conversation: MSConversation) {
// Called when the extension is about to move from the inactive to active state.
// This will happen when the extension is about to present UI.
if(conversation.selectedMessage?.url != nil) //trying to catch error
{
let components = URLComponents(string: (conversation.selectedMessage?.url?.query?.description)!)
//let val = conversation.selectedMessage?.url?.query?.description
if let queryItems = components?.queryItems {
// process the query items here...
let param1 = queryItems.filter({$0.name == "theirScore"}).first
print("***************=> GOT IT ",param1?.value)
}
}
When I just have 1 value, just by printing conversation.selectedMessage?.url?.query?.description I get an optional with that 1 value, which is good. But with multiple I cant find a clean way to get specific values by key.
What is the correct way to parse a URLQueryItem for given keys for iMessage?
When you do conversation.selectedMessage?.url?.query?.description it simply prints out the contents of the query. If you have multiple items then it would appear something like:
item=Item1&part=Part1&story=Story1
You can parse that one manually by splitting the string on "&" and then splitting the contents of the resulting array on "=" to get the individual key value pairs in to a dictionary. Then, you can directly refer to each value by key to get the specific values, something like this:
var dic = [String:String]()
if let txt = url?.query {
let arr = txt.components(separatedBy:"&")
for item in arr {
let arr2 = item.components(separatedBy:"=")
let key = arr2[0]
let val = arr2[1]
dic[key] = val
}
}
print(dic)
The above gives you an easy way to access the values by key. However, that is a bit more verbose. The way you provided in your code, using a filter on the queryItems array, is the more compact solution :) So you already have the easier/compact solution, but if this approach makes better sense to you personally, you can always go this route ...
Also, if the issue is that you have to write the same filtering code multiple times to get a value from the queryItems array, then you can always have a helper method which takes two parameters, the queryItems array and a String parameter (the key) and returns an optional String value (the value matching the key) along the following lines:
func valueFrom(queryItems:[URLQueryItem], key:String) -> String? {
return queryItems.filter({$0.name == key}).first?.value
}
Then your above code would look like:
if let queryItems = components?.queryItems {
// process the query items here...
let param1 = valueFrom(queryItems:queryItems, key:"item")
print("***************=> GOT IT ", param1)
}
You can use iMessageDataKit library. It makes setting and getting data really easy and straightforward like:
let message: MSMessage = MSMessage()
message.md.set(value: 7, forKey: "user_id")
message.md.set(value: "john", forKey: "username")
message.md.set(values: ["joy", "smile"], forKey: "tags")
print(message.md.integer(forKey: "user_id")!)
print(message.md.string(forKey: "username")!)
print(message.md.values(forKey: "tags")!)
(Disclaimer: I'm the author of iMessageDataKit)

What is the proper way to get values from NSDictionaries to separate variables

It's not entirely that I can't solve this, but there are proper and improper ways and I don't want to use a flawed method. I have this snapshot from firebase, and I declare it as NSDictionary as so:
let commentDict = commentsDictionary.value as? NSDictionary
When I print commentDict it looks like this:
commentText = Fififgkffy;
postedBy = USXhV0QYkpbSzAmXgfeteXHuoqg2;
commentText = Fhzifigh;
postedBy = USXhV0QYkpbSzAmXgfeteXHuoqg2;
CommentDict will always look like this. It will always have 2 items. I want to know the proper way to grab the items and assign them to variables. One way I have used before, but just on one item is by putting this in a for loop:
if name as! String == "postedBy" {
Then I get the value for the "postedBy" dictionary item. When having 2 items though, should I make a little array for commentText and postedBY, and have 2 arrays with 2 items each? Another important point is that These must stay in their own groups. I have to keep track of which "commentText" was the first group and which was the 2nd group, same with "postedBy".
In the long-run I have to end up with commentText1, postedBy1, commentText2, postedBy2 all as separate String variables.
This is what I ended up doing but I just want to know if there is a better or more "proper" way to do it. I might have to put some safety if statements into this method to make sure the array gets filled properly and all that.
if let commentDict = commentsDictionary.value as? NSDictionary {
for(name, value) in commentDict {
commentInfoArray.append(value as! String)
}
let comment1Text = commentInfoArray[0]
let comment1User = commentInfoArray[1]
let comment2Text = commentInfoArray[2]
let comment2User = commentInfoArray[3]
}
If it's only postedBy and commentText relation that you are looking for try using this as your structure:-
USXhV0QYkpbSzAmXgfeteXHuoqg2 : comment1 : true,
comment2 : true,
//postedById : [postText]
Then at your retrieval use a dictionary to append your data to:-
db_Ref.child(userID).observeSingleEvent(of: .value, with: {(snap) in
if let commentDict = snap.value as? [String:AnyObject]{
for each in commentDict{
print(each.key) // Append your key here
}
}
})