0

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.

Yrb
  • 8,103
  • 2
  • 14
  • 44
cosmiccat
  • 5
  • 2
  • Welcome to SO. Just a quick note that when asking Firestore questions, please include the structure in the actual question as an image. Links can break and if that happens, it would invalidate the question for future readers. I have updated your question with the structure. – Jay May 03 '22 at 17:49
  • A few things. The first and most import and is you cannot `return` from a Firebase function. There are a bunch of articles here on SO about that see [this Q&A](https://stackoverflow.com/questions/41262793/wait-for-firebase-to-load-before-returning-from-a-function). Second thing is Dictionaries are dynamic by nature but in this case it may be better to store your Firestore data as a codable object instead of working with a dictionary (which can be layers upon layers of key: value pairs). See [Firestore Codable](https://peterfriese.dev/posts/firestore-codable-the-comprehensive-guide/) – Jay May 03 '22 at 17:54
  • With regard to your comment on switching my firestore data to a codable object, do you mean the entire user or just the socials dictionary? Any advise on how to do this? Idk how much I would have to rewrite the functions and all. – cosmiccat May 03 '22 at 18:15
  • Your `user` object seems well suited to be codable. That being said, in many cases (queries for example) related user data is often better off stored in a separate collection, with the users uid being one of the fields. So you may want to consider breaking `socials` out into it's own collection that can be more readily queried etc. In that case a codable `social` object would work well. – Jay May 03 '22 at 18:51
  • But to make a codable struct for my socials dictionary, wouldn't that need to have pre-specified fields? Otherwise, how do I separate it into its own collection as you said so that I can do codable on it? – cosmiccat May 03 '22 at 22:12
  • Look at the documentation's "Custom Object" implementation. It might work better for you https://firebase.google.com/docs/firestore/manage-data/add-data#custom_objects – lorem ipsum May 04 '22 at 00:16
  • @loremipsum That's the documentation for Codable Objects; thanks for linking it! OP: Objects defined in code have `fields` where data is stored - those are 'pre-defined' and (can) match up to the field names used in Firestore. So, in what use case would you NOT want defined fields that match the data in Firestore? In other words, your `Socials` field looks like there are named fields you would want to use across all your social objects. No? – Jay May 04 '22 at 16:50
  • The idea behind socials would be for it to be a dictionary that can be extended dynamically. So, a user can add a facebook account, and then add another one if they wish. So I don't know how to build it with predefined values for social media fields since idk what the user will want to input. Unless i make a predefined list of possible socials to add, initialize them to empty strings, and then let the user modify. But that seems wasteful. Let me know if i understood your question correctly please. – cosmiccat May 04 '22 at 23:29
  • It can still be a dictionary... Something like `var socials:[String:String]` in a custom object. Not an approach I would take. I would go for an array of a `Social` model or something similar but a custom object would make your decoding and encoding much easier. Once you have the dictionary just search for "SwiftUI ForEach use dictionary" there are a few different approaches that are much cleaner than the approach that you are trying to use. – lorem ipsum May 05 '22 at 12:34

1 Answers1

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.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Thank you so much! For the part 2, would I have to have my "UserWithMapCodable" struct separate from my "User" struct? And would this still have the same firestore hierarchy where `socials` is a dictionary for each user, right? If so, how would I deal with having 2 separate IDs, being the user's UID which is a string and the codable's documentID? sorry again if these questions are elementary i really appreciate the help – cosmiccat May 06 '22 at 22:57
  • Also, would I have to remove the `init(data: [String:Any]) {}` initializer that i mentioned above? because then my mainviewmodel starts giving me errors inside of fetchCurrentUser(), specifically for `self.user = .init(data:data)`. It says "Cannot convert value of type '[String : Any]' to expected argument type 'User'". Should I remove entirely the init from my struct? – cosmiccat May 07 '22 at 00:01
  • @cosmiccat *would I have to have my "UserWithMapCodable" struct separate from my "User" struct* - no, that IS your user Struct; I just named it that to be clear it was a codable user. *would this still have the same firestore hierarchy where socials is a dictionary* - yes, as shown here `var socials: [String: String]?`. *how would I deal with having 2 separate IDs* - you can deal with it in whatever way works best for your queries - typically (and common practice) is to use the users uid as the documentId, then you don't need to store the uid as a field; the document can be directly accessed. – Jay May 07 '22 at 15:23
  • *Also, would I have to remove the initializer* - yes, it's not needed as the object (being codable) is initialized with that data. You don't need this either *self.user = .init(data:data)* - follow the code in the answer as this is all you need `let user = try! doc.data(as: UserWithMapCodable.self)` and the object is initialized correctly. When you have time take a look a that Firebase blog on codable (custom) objects [Mapping Firestore Data in Swift](https://peterfriese.dev/posts/firestore-codable-the-comprehensive-guide/) as it really explains the ins and outs of using codable. – Jay May 07 '22 at 15:27
  • Ok that makes sense, thank you. I think my last questions would be the following: the function `readCodableUserWithMap`, would i implement it inside of my MainViewModel (shown in the og problem) and then, whenever I need the dictionary, call it outside? Because I am wondering if i should initialize the user separately (ie what you showed, `let user = try! ....` and in another function call the dictionary. – cosmiccat May 07 '22 at 22:30
  • And on that note, I keep trying to call my dictionary in my foreach loop, but it still doesnt let me. I am doing: `ForEach(vm.user?.socials.sorted(by: >), id: \.key) { key, value in linkDisplay(social: key, handler: value) .listRowSeparator(.hidden) }.onDelete(perform: delete)` Should I be calling your function another way? – cosmiccat May 07 '22 at 22:31
  • Super good questions! They fall outside the scope of he original question and really should stand on their own so future readers can benefit from them as well (It's the essence of SO!). I would suggest implementing the code I presented and if it solves the question, accept the answer (as it addressed this question). Then post a separate question with your new code - which will really benefit others. – Jay May 08 '22 at 12:09
  • @cosmiccat The point at which you read in a user is dependent on when you need it - so sure! you could do it that way but it's not clear when it would be needed during the life of the running app (at start? At end? During?) - be sure to outline that in your new question(s). Code in comments is really hard to read - include that second question in your new question as well. Rule of thumb here on SO; if you're pasting code into comments there's a high chance it should actually be a separate question. – Jay May 08 '22 at 12:12
  • Thank you so much for the advise @Jay. I have published the new question here: https://stackoverflow.com/questions/72165354/how-to-make-foreach-loop-for-a-codable-swift-structs-dictionary-based-on-fires – cosmiccat May 08 '22 at 21:56
  • given that I changed my struct to be codable and used your advise for id being of type `@DocumentID`, how do i treat the pieces of my code (such as in mainviewmodel) where i reference uid, such as in the init? Similarly, now calling vm.user?.name or other fields come back empty. Because in your readCodableUserWithMap(), you made `let userID =` assignment empty, which originally I assume referred to my uid. Now that it's `@DocumentID`, what should it be? – cosmiccat May 09 '22 at 03:36
  • One last thing, now whenever I want to reference the id (given that before i was referencing uid), it says "value of type user has no member id"... – cosmiccat May 09 '22 at 04:29
  • @cosmiccat the documentId is the uid. Simple as that! See my answer [here](https://stackoverflow.com/questions/71172297/swift-retrieve-user-info-from-firebase-firebase-firestore/71195794#71195794) and maybe Dougs answer [here](https://stackoverflow.com/questions/62416439/making-document-id-uid-firestore-swift). So you don't really make other changes other than when you want to read in a specific user by their uid, you don't need to query for it - it can be read directly `usersCollection.document("uid_0").getDocument(completion:...` – Jay May 09 '22 at 18:41
  • But if the documentid is the uid, why is `let userID = ` empty in the `readCodableUserWithMap` you suggested? I am sorry if I am missing your point, this is very helpful but new to me. I will also look at more documentation. – cosmiccat May 09 '22 at 18:56
  • Note there is no `userID` in my models. The `documentID` maps to the `id` field in the model with `@DocumentID var id: String?` That's what mapping does - Firebase Codable knows that every document has a documentID and that's how you tell it where to stick that info – Jay May 09 '22 at 21:37
  • but in your code to map the Firestore data to the object you do have `userID`, which is what im confused about. – cosmiccat May 09 '22 at 21:54
  • No, just a typo. That was just a string holder for the uid when I copy and pasted from a project. It was originally `let userID = "uid_0"` and then I had `.document(userID)` - I removed it. Sorry for the confusion. – Jay May 09 '22 at 22:04
  • I implemented what you suggested, but it keeps crashing with your line of code. It says: – cosmiccat May 09 '22 at 22:25
  • hread 1: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.KeyedDecodingContainer, Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil)) – cosmiccat May 09 '22 at 22:25
  • @cosmiccat I understand. SO is a terribly bad interactive debugger so determining the issue through comments doesn't work. The code in my answer works as-is (copied from a working project). I would suggest implementing what I have, ensuring it works first, and then expanding it. When you get stuck, post a separate question with your new code and objects and we'll take a look. In the meantime, once you use that code and it works, don't forget to accept it so it can help others. See [Accepting](https://stackoverflow.com/help/someone-answers). Be sure to use my structure. – Jay May 10 '22 at 17:40
  • I ended up reverting and managed to fix what i wanted for printing out the dictionary. I posted the follow up on this link https://stackoverflow.com/questions/72190676/how-to-make-my-firebase-data-update-in-swift-in-realtime , thank you so much – cosmiccat May 11 '22 at 03:55
  • Be sure to accept this answer if it helped you - that will allow it to help future readers as well. – Jay May 11 '22 at 19:52