1

Firebase data-structure

  1. lastLocations ( batteryStatus, lat, long, timestamp, uid)
  2. profiles (name, phoneNumber, picture, uid)
  3. userFriends ( basis on the uid -> how many friends -> conversationUid, friendStatus, notify, phoneNumber, uid)

My Code:

Issues:

  • Not getting an idea how to fetch location & profile with friend list efficient way with observer so any change come it reflects. Firebase is asyncrohonous process.
  • observer implementation so data not completely load every-time

Results to achieve:

  • I need to show friends list ( name, profile picture, battery status, lat long(address), timeStamp ) on tableview on the basis of my uid.

Firebase JSON

{
  "lastLocations": {
    "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
      "batteryStatus": 22,
      "latitude": 40.9910537,
      "longitude": 29.020425,
      "timeStamp": 1556568633477,
      "uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1"
    },
    "zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
      "batteryStatus": 88,
      "latitude": 41.0173995,
      "longitude": 29.1406086,
      "timeStamp": 1571778174360,
      "uid": "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
    }
  },
  "profiles": {
    "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
      "fcmToken": "fp09-Y9ZAkQ:APA91bFgGB1phr4B9gZScnz7ngpqTb5MchgWRFjHmLCVmWGMJVsyFx0rtrz7roxzpE_MmuSaMc4is-XIu7j718qjRVCSHY4PvbNjL1LZ-iytaeDP0oa8aJgE02wET3cXqKviIRMH",
      "name": "Skander",
      "phoneNumber": "+95644125503",
      "uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1"
    },
    "zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
      "fcmToken": "enMneewiGgg:APA91bHyA4HypWUYhxGTUTTch8ZJ_6UUWhEIXRokmR-Y-MalwnrtV_zMsJ9p-sU_ZT4pVIvkmtJaCo7LFJYJ9ggfhc1f2HLcN9AoIevEBUqyoMN-HDzkweiUxAbyc84XSQPx7RZ1Xv",
      "name": "Murad",
      "phoneNumber": "+915377588674",
      "picture": "profile/zzV6DQSXUyUkPHgENDbZ9EjXVBj2/a995c7f3-720f-45bf-ac58-b2df934e3dff.jpeg",
      "uid": "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
    }
  },
  "userFriends": {
    "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
      "zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
        "conversationUid": "-L_w2yi8gh49GppDP3r5",
        "friendStatus": "STATUS_ACCEPTED",
        "notify": true,
        "phoneNumber": "+915377588674",
        "uid": "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
      }
    },
    "zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
      "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
        "conversationUid": "-L_w2yi8gh49GppDP3r5",
        "friendStatus": "STATUS_ACCEPTED",
        "notify": true,
        "phoneNumber": "+915644125503",
        "uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1"
      }
    }
  }

}

Swift Function:

func getFrndDataList(){
        AppData.removeAll()
        ref.child("userFriends").child("zzV6DQSXUyUkPHgENDbZ9EjXVBj2").observe(.childAdded, with: { (snapshot) in

           guard let data = try? JSONSerialization.data(withJSONObject: snapshot.value as Any) else { return }
           let frndList = try? JSONDecoder().decode(Friend.self, from: data)

           self.AppData.append(frndList!)
           self.tableView.reloadData()
           print([frndList])
        })
    }
Jay
  • 34,438
  • 18
  • 52
  • 81
Newbie
  • 360
  • 3
  • 19
  • There are about 10 different ways to tackle this and I will post an answer. One question: why is each users data being stored in separate nodes? You've got users location data stored in the */lastLocations* node, and then name and phone number stored in the */profiles* node and then friend status stored in the */userFriends* node. It seems like all of that data could just be stored within a `/users/uid/this_users_info` node. Changing the structure would make it more maintainable, require less code and less access to Firebase (reducing cost). – Jay Oct 24 '19 at 19:15
  • @Jay I haven't designed this database structure personally. Even I have discussed and highlighted this issue to change the db structure `lastlocations & profile nodes` must be stored in `userFriend` as you already mentioned but the answer I got in this way we only targeted to specific nodes in case of change/delete. – Newbie Oct 24 '19 at 19:49
  • Hmmm. You are only targeting specific nodes in case of change/delete in any case. In other words, if the users batteryStatus changes, you will need to update the battery status within their */lastLocations/uid* node. If it were all together, you would have to update the battery status in the */users/uid node*. It has to be updated regardless of where it is so there's no real difference. – Jay Oct 24 '19 at 20:43
  • Posted the longest answer evah, hope it helps. lol. Let me know if you need clarification. – Jay Oct 24 '19 at 21:43

1 Answers1

1

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.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Really thank you so much I know you have put really great effort for me and I really appreciate it. Let me ask about data structure and properly implement code accordingly and get back to you. However about `"friendStatus": "STATUS_ACCEPTED",` I have to use it but you mentioned I will add them in userFriends node but above we also have `true value` Do I need to use `uid` twice `FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: "STATUS_ACCEPTED" . FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: true` like this ? – Newbie Oct 25 '19 at 07:20
  • 1
    @Newbie The true value is really a placeholder - a node cannot exist without a value so using true is a simple way to do that. It could be any value and represent any data you want - it just needs to be there. – Jay Oct 25 '19 at 14:05
  • @ Thank you again. Let me implement all scenario and get back to you. :) – Newbie Oct 25 '19 at 14:12
  • Morning. Today I have to present all back end structure and display content. I would probably suggest about new data structure in firebase. I have properly setup functions and tweaking as required done. Working successfully and it's all because of you. Thank you. I have one question in `func watchForChangesInMyFriends()` how can I update model if changes occur and reload tableview. I try to implement code to update model but no success. – Newbie Oct 28 '19 at 08:11
  • @Newbie The snapshot will contain the new information so you would read the info from the snapshot, update that object with the new info in your array and then reload the tableView. So after this `let friend = self.myFriendsDataSource[friendIndex]` you would then replace the properties in the friend object with the data from the snapshot. – Jay Oct 28 '19 at 16:47
  • Yes I am doing the same thing but not reflecting updated values in tableview. let me post my function. `func watchForChangesInMyFriends() { let usersRef = self.ref.child("profiles") 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.batteryStatus), updating") self.myFriendsDataSource[friendIndex] = friend self.tableView.reloadData() } }) }` – Newbie Oct 29 '19 at 10:41
  • 1
    @Newbie posting code in comments is generally discouraged as it's very hard to read. If you are having difficulty with a piece of code you should do two things. First, if this answer helped, accept it so it can help others [How To Accept](https://stackoverflow.com/help/someone-answers). Then post a separate question with the code your are having difficulty with and an explanation of what you're attempting and what the issue is with the code. You may want to link that question here since it's somewhat related to this answer. – Jay Oct 29 '19 at 14:54
  • Thank you so much again. I accepted your answer and posted new question regarding updating object in array. https://stackoverflow.com/questions/58623750/update-that-object-with-the-new-info-in-array-and-to-display-tableview-swift – Newbie Oct 30 '19 at 10:53