Firebase and Reading Nested Data Using Swift

How to read nested data from realtime database firebase

The main issue is the Firebase structure presented in the question doesn't match the code:

user_id
gas_station_id
name

and the code is attempting to read it like this

if let gasStationDict = snapshot.value as? [String:String]

Where the structure is more like this [String: [String: String]] or possibly [String: [String: Int]]

I suggest another solution using .childSnapshot to get deeply nested data. It's easier to read and way easier to maintain

I would also suggest changing the structure. In NoSQL databases it's often best practice to disassociate node keys from the data they contain.

For example, in your structure, 6642 is station ID. What if, in the future the station ID changes? You would literally have to go through your entire database, search for that, delete those nodes and whatever they contain and then re-write them. ugh!

Here's a better structure

{
"username123" : {
a_firebase_generated_node_key : { //using .childByAutoId to create
"latitude" : 37.861639,
"longitude" : -4.785556,
"name" : "CEPSA"
"station_id: "6652"
}
}
}

now you can change any aspect of the station and won't have to change any references to it.

Then the code to read and print all of the stations using childSnapshot

func printUsersGasStations() {
let ref = self.ref.child("gas_stations").child("username123")
ref.getData(completion: { error, snapshot in
if let err = error {
print(err.localizedDescription)
return
}

//this next line takes all of the stations and creates
// an ordered array of them as DataSnapshots
let allStationsSnap = snapshot.children.allObjects as! [DataSnapshot]

for stationSnap in allStationsSnap {
let stationId = stationSnap.childSnapshot(forPath: "station_id").value as? String ?? "No Station ID"
let lat = stationSnap.childSnapshot(forPath: "lat").value as? Double ?? 0.0
let lon = stationSnap.childSnapshot(forPath: "lon").value as? Double ?? 0.0
let name = stationSnap.childSnapshot(forPath: "name").value as? String ?? "No Name"
print(stationId, lat, lon, name)
}
})
}

Getting nested data in firebase using swift

here is the solution

let comments = messages.childSnapshot(forPath: "comments").value as? [String: Any] ?? [:]

for comment in comments {

let theComment = comment.value as? [String: Any]

let theContent = theComment?["content"] as? String ?? ""
let theIcon = theComment?["icon"] as? String ?? ""
let theColor = theComment?["color"] as? String ?? ""
let theDate = theComment?["date"] as? String ?? ""
let theName = theComment?["userName"] as? String ?? ""

let aComment = messageComments(content: theContent, color: theColor, icon: theIcon, date: theDate, userName: theName)
commentArray.append(aComment)
}

How to fetch nested data from Firebase realtime database by implementing observer in swift

Note: After writing this answer I realized it was way long but this is a big question and there are a lot of elements to address.

My first suggestion is to change the structure as it's overly complicated for what's being done with the data. Also, there is repetitive data that's not needed so that should be changed as well. For example, here's your profiles node

  "profiles": {
"FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
"fcmToken": "fp09-Y9ZAkQ:APA91bFgGB1phr4B9gZScnz7ngpqTb5MchgWRFjHmLCVmWGMJVsyFx0rtrz7roxzpE_MmuSaMc4is-XIu7j718qjRVCSHY4PvbNjL1LZ-iytaeDP0oa8aJgE02wET3cXqKviIRMH",
"name": "Skander",
"phoneNumber": "+95644125503",
"uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1" <- remove this, not needed.
},

As you can see, each child node has a key of the user id. But, you are also storing the user id as a child node as well. They key is the uid and will always be available so no need for duplication there and the child node should be removed.

Based on comments, this is a much better structure

/users
FTgzbZ9uWBTkiZK9kqLZaAIhEDv1
"batteryStatus": 22,
"latitude": 40.9910537,
"longitude": 29.020425,
"timeStamp": 1556568633477,
"fcmToken": "fp09-Y9ZAkQ:APA91bFgGB1phr4B9gZScnz7ngpqTb5MchgWRFjHmLCVmWGMJVsyFx0rtrz7roxzpE_MmuSaMc4is-XIu7j718qjRVCSHY4PvbNjL1LZ-iytaeDP0oa8aJgE02wET3cXqKviIRMH",
"name": "Skander",
"phoneNumber": "+95644125503",
"conversationUid": "-L_w2yi8gh49GppDP3r5",
"friendStatus": "STATUS_ACCEPTED",
"notify": true,
"phoneNumber": "+915377588674",

and then, to keep track of a users friends, it becomes this

/userFriends
zzV6DQSXUyUkPHgENDbZ9EjXVBj2 //this user
FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: true //their friend
IRoo0lbhaihioSSuFETngEEFEeoi: true //another friend

To load this users friends, we read the data at /userFriends/this_users_id and then iterate over the child nodes loading the data for display in the tableView

Lets start with an object that will be used to hold each friends data, and then an array that will be used as a tableView Datasource

class FriendClass {
var uid = ""
var name = ""
//var profilePic
//var batteryStatus

init(withSnapshot: DataSnapshot) {
self.uid = withSnapshot.key
self.name = withSnapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
}
}

var myFriendsDataSource = [FriendClass]()

Then a functions to read the users node, iterate over the users friends uid's and read in each users data, populating the FriendClass object and storing each in an array. Note that self.ref points to my firebase.

func loadUsersFriends() {
let uid = "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
let myFriendsRef = self.ref.child("userFriends").child(uid)
myFriendsRef.observeSingleEvent(of: .value, with: { snapshot in
let uidArray = snapshot.children.allObjects as! [DataSnapshot]
for friendsUid in uidArray {
self.loadFriend(withUid: friendsUid.key)
}
})
}

func loadFriend(withUid: String) {
let thisUserRef = self.ref.child("users").child(withUid)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let aFriend = FriendClass(withSnapshot: snapshot)
self.myFriendsDataSource.append(aFriend)
})
}

Now that we have the code to read in the data, you also want to watch for changes. There are a number of options but here's two.

1) I'll call this brute force.

Simply attach a .childChanged observer to the /users node and if something changes, that changed node is passed to the observer. If the key to that node matches a key in myFriendsDataSource array, update that user in the array. If no match, then ignore it.

func watchForChangesInMyFriends() {
let usersRef = self.ref.child("users")
usersRef.observe(.childChanged, with: { snapshot in
let key = snapshot.key
if let friendIndex = self.myFriendsDataSource.firstIndex(where: { $0.uid == key} ) {
let friend = self.myFriendsDataSource[friendIndex]
print("found user \(friend.name), updating")
//friend(updateWithSnapshot: snapshot) //leave this for you to code
}
})
}

2) Selective observing

For this, we simply attach an .childChanged observer to each friend node - and that can be done within the code example from above

func loadFriend(withUid: String) {
let thisUserRef = self.ref.child("users").child(withUid)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let aFriend = FriendClass(withSnapshot: snapshot)
self.myFriendsDataSource.append(aFriend)
//add an observer to this friends node here.
})
}

One last thing: I didn't address this

"friendStatus": "STATUS_ACCEPTED",

I would think that only friends you accepted are in the friends list so the use is a tad unclear. However, if you want to use it you could do this

/userFriends
zzV6DQSXUyUkPHgENDbZ9EjXVBj2 //this user
FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: "STATUS_ACCEPTED"
IRoo0lbhaihioSSuFETngEEFEeoi: "STATUS_DECLINED"

and then as you're itering over friends to load, ignore the ones that are declined.

If you MUST keep your current structure (which I do NOT recommend) the techniques in this answer will work for that structure as well, however, it will be a lot more code and you're going to be moving around a lot of unneeded extra data so the Firebase bill will be higher.

Firebase and reading nested data using Swift

var postsCommentsDict : NSMutableDictionary = NSMutableDictionary()
var userNameArray : [String] = [String]()
var userCommentArray : [String] = [String]()
FIRDatabase.database.reference().child("Posts").observeEventType(.Value, withBlock: {(Snapshot) in

if Snapshot.exists(){
let imageD = Snapshot.value
let imageD_ID = imageD.key
//Retrieving the email and image name
let imageName = imageD["userImage"] as! String
let userEmail = imageD["userEmail"] as! String
//Here you are accessing each image ID
//First Question Alternative Completed

//To answer the second Question :-

FIRDatabase.database.reference().child("Posts").child(imageD_ID).child("comments").observeEventType(.Value, withBlock: {(Snapshot) in

if let commentsDictionary = Snapshot.value {

for each in commentsDictionary{

postsCommentsDict.setObject(each["userName"] as! String , forKey : each["userComment"] as! String)
//Saving the userName : UserComment in a dictionary
userNameArray.append(each["userName"] as! String)
userCommentArray.append(each["userComment"] as! String)
//Saving the details in arrays
//Prefer dictionary over Arrays

}

} else {

//no comments to append the arrays

}

})

}
})

Once you are Done Saving the Comments dictionary : How to read it : -

for each in postsCommentsDict as! NSDictionary{
let userNm = each.key
let userComment = each.value
//username and userComment's retrieved

}

Please ignore the Typos, if any!...hope this helps..

Swift 4 / Firebase - Reading and storing double nested items from realtime databse in different arrays

Here's the complete code to read all of the data in the Live node, and put Today, Tomorrow and ThisWeek events into separate array's

var todayArray = [String]()
var tomorrowArray = [String]()
var thisWeekArray = [String]()

let liveRef = self.ref.child("Live")
liveRef.observeSingleEvent(of: .value, with: { snapshot in

let todaySnap = snapshot.childSnapshot(forPath: "Today")
for todayChild in todaySnap.children {
let todayChildSnap = todayChild as! DataSnapshot
let todayDict = todayChildSnap.value as! [String: Any]
let title = todayDict["title"] as! String
todayArray.append(title)
}

let tomorrowSnap = snapshot.childSnapshot(forPath: "Tomorrow")
for tomorrowChild in tomorrowSnap.children {
let tomorrowChildSnap = tomorrowChild as! DataSnapshot
let tomorrowDict = tomorrowChildSnap.value as! [String: Any]
let title = tomorrowDict["title"] as! String
tomorrowArray.append(title)
}

let thisWeekSnap = snapshot.childSnapshot(forPath: "ThisWeek")
for thisWeekChild in thisWeekSnap.children {
let thisWeekChildSnap = thisWeekChild as! DataSnapshot
let thisWeekDict = thisWeekChildSnap.value as! [String: Any]
let title = thisWeekDict["title"] as! String
thisWeekArray.append(title)
}

print("Today")
for today in todayArray {
print(" " + today)
}

print("Tomorrow")
for tomorrow in tomorrowArray {
print(" " + tomorrow)
}

print("This Week")
thisWeekArray.map { print(" " + $0) } //gettin' all swifty here
})

and the output is

Today
rnd
arnd
Tomorrow
brnd
This Week
drnd
crnd

However... I would probably change the stucture:

Live
event_0 //generated with childByAutoId
title: "rnd"
timestamp: "20180918"
event_1
title: "brnd"
timestamp: "20180919"

because you can now query on every node, pulling out today's events. Tomorrow's events or ThisWeeks event or any day or range you like.

I'm going to uploading new data each day so today, tomorrow and this
week will be refreshed each day

and with this structure, all you would need to do is add nodes and your queries will figure out what nodes are for what periods of time.

Since your Event class doesn't do any processing and essentially just a structure to hold your data, how about a struct:

struct EventStruct {
var EventTitle: String?
var EventDescription: String?
var EventStamp: Int?
}

Then add the events to the array:

let title = thisWeekDict["title"] as! String
let desc = thisWeekDict["desc"] as! String
let stamp = thsWeekDict["stamp"] as! String
let anEvent = Event(title: title, desc: desc, stamp: stamp)
thisWeekArray.append(anEvent)

or leave it as a class and just pass the snapshot in and have the class assign the properties from the snapshot. Either way, please add error checking for nil's.

Fetch nested array Firebase + SwiftUI

Please do yourself a favour and don't map data manually from Firestore to Swift.

Firestore supports the Swift Codable protocol, which makes things a lot easier.

Using Codable w/ Firestore

Here's a short snippet that shows how to adapt your code:

struct Order: Identifiable, Codable {
@DocumentID var id: String
var number: Int
var status: String
var clientName: String
// ...
}
class OrdersViewModel: ObservableObject {
@Published var orders = [Order]()
@Published var errorMessage: String?

private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?

public func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}

func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("orders")
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.errorMessage = "No documents in 'orders' collection"
return
}

self?.orders = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: Order.self) }

switch result {
case .success(let order):
if let order = order {
// An Order value was successfully initialized from the DocumentSnapshot.
self?.errorMessage = nil
return order
}
else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
// An Order value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}

}

While this might look more complicated than your code, the key line is this:

try queryDocumentSnapshot.data(as: Order.self)

The rest of the code performs some error handling (which you should absolutely do), and thus adds a bit of noise.

Mapping arrays

Using Codable makes mapping arrays quite simple. You can use any type that is Codable itself inside your arrays.

So for your order items, use the following:

struct OrderItem: Codable {
var cost: Int
var name: String
}

And then update the Order struct as follows:

struct Order: Identifiable, Codable {
@DocumentID var id: String
var number: Int
var status: String
var clientName: String
var items: [OrderItem]
// ...
}

That's it - everything else will be handles automatically. See Mapping arrays in my article for further details.

Resources

For more details, please refer to Mapping Firestore Data in Swift - The Comprehensive Guide and SwiftUI: Mapping Firestore Documents using Swift Codable - Application Architecture for SwiftUI & Firebase. The article also explains how to customize the mapping using CodingKeys.



Related Topics



Leave a reply



Submit