'How do I read a User's Firestore Map to a Swift Dictionary?

I have my user struct with has a dictionary of all their social medias.

struct User: Identifiable {

var id: String { uid }

let uid, email, name, bio, profileImageUrl: String
let numSocials, followers, following: Int

var socials: [String: String]


init(data: [String: Any]) {
    self.uid = data["uid"] as? String ?? ""
    self.email = data["email"] as? String ?? ""
    self.name = data["name"] as? String ?? ""
    self.bio = data["bio"] as? String ?? ""
    self.profileImageUrl = data["profileImageURL"] as? String ?? ""
    
    self.numSocials = data["numsocials"] as? Int ?? 0
    self.followers = data["followers"] as? Int ?? 0
    self.following = data["following"] as? Int ?? 0
    
    self.socials = data["socials"] as? [String: String] ?? [:]

}
}

The idea is for socials (the dictionary), to be dynamic, since users can add and remove social medias. Firestore looks like this:

enter image description here

The dictionary is initialized as empty. I have been able to add elements to the dictionary with this function:

private func addToStorage(selectedMedia: String, username: String) -> Bool {
    if username == "" {
        return false
    }
    guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
        print("couldnt get uid")
        return false
    }
    
    FirebaseManager.shared.firestore.collection("users").document(uid).setData([ "socials": [selectedMedia:username] ], merge: true)

    print("yoo")
    return true
}

However I can't seem to read the firestore map into my swiftui dictionary. I want to do this so that I can do a ForEach loop and list all of them. If the map is empty then the list would be empty too, but I can't figure it out.

Just in case, here is my viewmodel.

class MainViewModel: ObservableObject {

@Published var errorMessage = ""
@Published var user: User?

init() {
    DispatchQueue.main.async {
        self.isUserCurrentlyLoggedOut = FirebaseManager.shared.auth.currentUser?.uid == nil
    }

    fetchCurrentUser()
    
}

func fetchCurrentUser() {
    guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
        self.errorMessage = "Could not find firebase uid"
        print("FAILED TO FIND UID")
        return

    }

    FirebaseManager.shared.firestore.collection("users").document(uid).getDocument { snapshot, error in
        if let error = error {
            self.errorMessage =  "failed to fetch current user: \(error)"

            print("failed to fetch current user: \(error)")
            return
        }
        guard let data = snapshot?.data() else {
            print("no data found")
            self.errorMessage = "No data found"
            return

        }


        self.user = .init(data: data)

    }
}
   
}

TLDR: I can't figure out how to get my firestore map as a swiftui dictionary. Whenever I try to access my user's dictionary, the following error appears. If I force unwrap it crashes during runtime. I tried to coalesce with "??" but I don't know how to make it be the type it wants.

ForEach(vm.user?.socials.sorted(by: >) ?? [String:String], id: \.key) { key, value in
                    linkDisplay(social: key, handler: value)
                        .listRowSeparator(.hidden)


                }.onDelete(perform: delete)

error to figure out

Please be patient. I have been looking for answers through SO and elsewhere for a long time. This is all new to me. Thanks in advance.



Solution 1:[1]

This is a two part answer; Part 1 addresses the question with a known set of socials (Github, Pinterest, etc). I included that to show how to map a Map to a Codable.

Part 2 is the answer (TL;DR, skip to Part 2) so the social can be mapped to a dictionary for varying socials.

Part 1:

Here's an abbreviated structure that will map the Firestore data to a codable object, including the social map field. It is specific to the 4 social fields listed.

struct SocialsCodable: Codable {
    var Github: String
    var Pinterest: String
    var Soundcloud: String
    var TikTok: String
}

struct UserWithMapCodable: Identifiable, Codable {
    @DocumentID var id: String?
    var socials: SocialsCodable? //socials is a `map` in Firestore
}

and the code to read that data

func readCodableUserWithMap() {
    let docRef = self.db.collection("users").document("uid_0")

    docRef.getDocument { (document, error) in
        if let err = error {
            print(err.localizedDescription)
            return
        }

        if let doc = document {
            let user = try! doc.data(as: UserWithMapCodable.self)
            print(user.socials) //the 4 socials from the SocialsCodable object
        }
    }
}

Part 2:

This is the answer that treats the socials map field as a dictionary

struct UserWithMapCodable: Identifiable, Codable {
    @DocumentID var id: String?
    var socials: [String: String]?
}

and then the code to map the Firestore data to the object

func readCodableUserWithMap() {
    let docRef = self.db.collection("users").document("uid_0")

    docRef.getDocument { (document, error) in
        if let err = error {
            print(err.localizedDescription)
            return
        }

        if let doc = document {
            let user = try! doc.data(as: UserWithMapCodable.self)
            if let mappedField = user.socials {
                mappedField.forEach { print($0.key, $0.value) }
            }
        }
    }
}

and the output for part 2

TikTok ogotok
Pinterest pintepogo
Github popgit
Soundcloud musssiiiccc

I may also suggest taking the socials out of the user document completely and store it as a separate collection

socials
   some_uid
      Github: popgit
      Pinterest: pintepogo
   another_uid
      Github: git-er-done
      TikTok: dancezone

That's pretty scaleable and allows for some cool queries: which users have TikTok for example.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1