如何通过在 swift 中实现观察者从 Firebase 实时数据库中获取嵌套数据

Posted

技术标签:

【中文标题】如何通过在 swift 中实现观察者从 Firebase 实时数据库中获取嵌套数据【英文标题】:How to fetch nested data from Firebase realtime database by implementing observer in swift 【发布时间】:2019-10-24 09:18:14 【问题描述】:

Firebase 数据结构

    lastLocations(batteryStatus、lat、long、timestamp、uid) 个人资料(姓名、电话号码、图片、uid) userFriends (根据 uid -> 有多少朋友 -> conversationUid,friendStatus, notify, phoneNumber, uid)

我的代码:

我已经为它创建了tableview 和xib。 我已经为最后一个位置、个人资料、用户朋友创建了模型。 我已经获取了好友列表,但在 Observe .ChildAdded 上 我的用户名zzV6DQSXUyUkPHgENDbZ9EjXVBj2 链接:https://drive.google.com/file/d/19cnkY03MXjrTFgzzPCdvmOuosDRvoMx9/view?usp=sharing

问题:

不知道如何通过观察者有效地使用好友列表获取位置和个人资料,因此任何变化都会反映出来。 Firebase 是异步进程。 观察者实现,因此数据不会每次都完全加载

要达到的结果:

我需要根据我的 uid 在 tableview 上显示好友列表(姓名、头像、电池状态、经纬度(地址)、时间戳)。

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"
      
    
  


快速函数:

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])
        )
    

【问题讨论】:

有大约 10 种不同的方法来解决这个问题,我会发布一个答案。一个问题:为什么每个用户的数据都存储在不同的节点中?您将用户位置数据存储在 /lastLocations 节点中,然后将姓名和电话号码存储在 /profiles 节点中,然后将朋友状态存储在 中/userFriends 节点。似乎所有这些数据都可以存储在/users/uid/this_users_info 节点中。更改结构将使其更易于维护,需要更少的代码和更少的 Firebase 访问(降低成本)。 @Jay 我没有亲自设计过这个数据库结构。即使我已经讨论并强调了这个问题,以更改数据库结构lastlocations & profile nodes 必须存储在userFriend 中,正如您已经提到的但我以这种方式得到的答案我们只针对特定节点以防更改/删除。跨度> 嗯。在任何情况下,您都只针对特定节点进行更改/删除。换句话说,如果用户的电池状态发生变化,您将需要在他们的 /lastLocations/uid 节点中更新电池状态。如果它们都在一起,则必须在 /users/uid 节点 中更新电池状态。无论它在哪里都必须更新,所以没有真正的区别。 发表了最长的答案evah,希望对您有所帮助。哈哈。如果您需要澄清,请告诉我。 【参考方案1】:

注意:写完这个答案后,我意识到它很长,但这是一个大问题,需要解决很多问题。

我的第一个建议是更改结构,因为它对于处理数据的操作过于复杂。此外,还有一些不需要的重复数据,因此也应该更改。例如,这是您的个人资料节点

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

如您所见,每个子节点都有一个用户 ID 的键。但是,您也将用户 ID 存储为子节点。它们的关键是 uid,并且将始终可用,因此无需在此处复制,应删除子节点。

基于cmets,这是一个更好的结构

/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",

然后,为了跟踪用户的朋友,它变成了这个

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

为了加载这个用户的朋友,我们在 /userFriends/this_users_id 读取数据,然后遍历子节点加载数据以在 tableView 中显示

让我们从一个用于保存每个朋友数据的对象开始,然后是一个将用作 tableView 数据源的数组

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]()

然后是读取用户节点的函数,遍历用户好友 uid 并读取每个用户数据,填充 FriendClass 对象并将每个对象存储在数组中。请注意 self.ref 指向我的火力基地。

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)
    )

现在我们已经有了读取数据的代码,您还想观察变化。有很多选择,但这里有两个。

1) 我称之为蛮力。

只需将 .childChanged 观察者附加到 /users 节点,如果发生变化,则将更改的节点传递给观察者。如果该节点的键与 myFriendsDataSource 数组中的键匹配,则更新数组中的该用户。如果不匹配,则忽略它。

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) 选择性观察

为此,我们只需将 .childChanged 观察者附加到每个朋友节点 - 这可以在上面的代码示例中完成

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.
    )

最后一件事:我没有解决这个问题

"friendStatus": "STATUS_ACCEPTED",

我认为只有您接受的朋友才会在朋友列表中,因此用途有点不清楚。但是,如果你想使用它,你可以这样做

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

然后当您遍历要加载的朋友时,忽略那些被拒绝的朋友。

如果您必须保留当前的结构(我不推荐),则此答案中的技术也适用于该结构,但是,它会包含更多代码,并且您会经常移动不需要的额外数据,因此 Firebase 费用会更高。

【讨论】:

真的非常感谢你,我知道你为我付出了巨大的努力,我真的很感激。让我询问数据结构并相应地正确实现代码并回复您。但是关于"friendStatus": "STATUS_ACCEPTED", 我必须使用它,但你提到我会将它们添加到 userFriends 节点中,但上面我们还有true value 我需要像这样使用uid 两次FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: "STATUS_ACCEPTED" . FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: true 吗? @Newbie true 值实际上是一个占位符 - 没有值的节点不能存在,因此使用 true 是一种简单的方法。它可以是任何值并代表您想要的任何数据 - 它只需要在那里。 @再次感谢您。让我实现所有场景并回复您。 :) 早上。今天我要介绍所有的后端结构和显示内容。我可能会建议关于 firebase 中的新数据结构。我已经正确设置了功能并根据需要进行了调整。工作顺利,都是因为你。谢谢你。我在func watchForChangesInMyFriends() 有一个问题,如果发生更改并重新加载表格视图,我该如何更新模型。我尝试实现代码来更新模型但没有成功。 @Newbie 在 cmets 中发布代码通常不被鼓励,因为它很难阅读。如果你对一段代码有困难,你应该做两件事。首先,如果这个答案有帮助,请接受它,以便它可以帮助其他人How To Accept。然后发布一个单独的问题,其中包含您遇到困难的代码,并解释您正在尝试的内容以及代码的问题。您可能想在此处链接该问题,因为它与此答案有些相关。

以上是关于如何通过在 swift 中实现观察者从 Firebase 实时数据库中获取嵌套数据的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C++ 中实现观察者设计模式流数据?

Swift:通过 NSNotificationCenter 的键盘观察器不起作用

如何在使用firebase作为服务器或数据库的cordova应用程序中实现通知

如何在 swift 中实现类似 Dropbox 的进度视图?

如何在 Swift 中实现线程安全的 HashTable (PhoneBook) 数据结构?

如何在Swift中实现Objective-C而不定义新类