SwiftUI App can't get data from Firestore - swift

I will start by explaining with words, then I will explain with code below. Here we go.
I have a struct 'User' that I use in my app. It has important data like username, email, and profileImageUrl. To access a list of users, I need to request a list of users from Firestore. But when I do this, it returns an empty array.
EXCEPT right after I log in. If I check the Users array before I log in, it's empty. If I check the users array after I log in, it's empty. But if I print it inside the .onAppear of the main content view that loads, it prints the true array of users.
I also notice that my program never thinks I'm the current user, even if I am. Each instance of User has a property isCurrentUser: Bool that is supposed to check if the user that is currently logged in is the same user in that instance. But they all appear as false.
It seems to me that the program thinks I'm not logged in, and will only let me pull data from the server if I'm logged in. Even though I changed my Firestore and Firebase Storage rules to allow me to read and write even if I'm not logged in.
Here are important snippets of code.
User struct
struct User : Identifiable {
let id : String
let username : String
let profileImageUrl : String
let email : String
let isCurrentUser: Bool
init (dictionary: [String: Any]) {
self.id = dictionary["uid"] as? String ?? ""
self.username = dictionary["username"] as? String ?? ""
self.profileImageUrl = dictionary["profileImageUrl"] as? String ?? ""
self.email = dictionary["email"] as? String ?? ""
self.isCurrentUser = Auth.auth().currentUser?.uid == self.id
}
}
notice the self.isCurrentUser code. I'm thinking maybe this code isn't working, or that self.id isn't working properly.
I also receive these two errors whenever I unsuccessfully fetch from the Users array.
2021-02-24 21:00:50.172361-0800 Council[38163:1564633] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
2021-02-24 21:00:52.168254-0800 Council[38163:1564630] [connection] nw_resolver_start_query_timer_block_invoke [C1] Query fired: did not receive all answers in time for firebaselogging-pa.googleapis.com:443
So please share any knowledge you have this problem is killing me i have to finish this app in time and now I am not able to work due to this bug
Here is my main ContentView that shows login screen if a user isn't logged in, but shows the homepage if a user is logged in. I called an instance of SearchViewModel() to test if I could print SearchViewModel().users. This is the view in which I can get the users array to print, but only right after I log in. If I attempt to print it on a subsequent page, it'll be the same empty array.
import SwiftUI
struct ContentView: View {
#State private var navBarHidden: Bool = true
#EnvironmentObject var viewModel : AuthViewModel
#ObservedObject var searchViewModel = SearchViewModel()
var body: some View {
Group{
if viewModel.userSession != nil{
NavigationView{
TabView{
FeedView(navBarHidden: $navBarHidden)
.navigationBarHidden(self.navBarHidden)
.tabItem{
Image(systemName: "house")
Text("For You")
}
Text("Random")
.tabItem{
Image(systemName: "questionmark.circle")
Text("Random")
}
SearchView()
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
.navigationBarHidden(true)
}
.accentColor(.black)
.onAppear() {
searchViewModel.fetchUsers()
print(searchViewModel.users)
}
}
} else {
LoginView()
}
}
}
}
More code: Here is my User struct
import Foundation
import Firebase
struct User : Identifiable {
let id : String
let username : String
let profileImageUrl : String
let email : String
let isCurrentUser: Bool
init (dictionary: [String: Any]) {
self.id = dictionary["uid"] as? String ?? ""
self.username = dictionary["username"] as? String ?? ""
self.profileImageUrl = dictionary["profileImageUrl"] as? String ?? ""
self.email = dictionary["email"] as? String ?? ""
self.isCurrentUser = Auth.auth().currentUser?.uid == self.id
}
}
Here is the file for the ViewModel that fetches the users
import SwiftUI
import Firebase
class SearchViewModel: ObservableObject {
#Published var users = [User]()
init() {
fetchUsers()
print("usersFetched")
}
func fetchUsers() {
Firestore.firestore().collection("users").getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else {return}
documents.forEach{ document in
let user = User(dictionary: document.data())
self.users.append(user)
}
}
}
Here is the ViewModel that authenticates users, creates users, and logs them in
import SwiftUI
import Firebase
class AuthViewModel: ObservableObject {
#Published var userSession: FirebaseAuth.User?
#Published var isAuthenticating = false
#Published var error: Error?
#Published var user: User?
init() {
userSession = Auth.auth().currentUser
fetchUser()
}
func login(withEmail email : String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error{
print("Failed to upload image. Error: \(error.localizedDescription)")
return
}
self.userSession = result?.user
print("U logged in")
}
}
func registerUser( email : String, password: String, username: String, profileImage: UIImage) {
guard let imageData = profileImage.jpegData(compressionQuality: 0.3) else {return}
let fileName = NSUUID().uuidString
let storageRef = Storage.storage().reference().child(fileName)
storageRef.putData(imageData, metadata: nil) { _, error in
if let error = error{
print("Failed to upload image. Error: \(error.localizedDescription)")
return
}
print("successful upload of photo")
storageRef.downloadURL { url , _ in
guard let profileImageUrl = url?.absoluteString else {return}
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("Failed to Register. Error: \(error.localizedDescription)")
return
}
guard let user = result?.user else {return}
let data = ["email": email,
"username": username.lowercased(),
"profileImageUrl": profileImageUrl,
"uid": user.uid]
Firestore.firestore().collection("users").document(user.uid).setData(data) { _ in
self.userSession = user
print("successfully uploaded user data")
}
print("Successful SIgnup")
}
}
}
}
func signOut() {
userSession = nil
try? Auth.auth().signOut()
}
func fetchUser() {
guard let uid = userSession?.uid else {return}
Firestore.firestore().collection("users").document(uid).getDocument { snapshot, _ in
guard let data = snapshot?.data() else {return}
let user = User(dictionary: data)
}
}
}
EDIT: New possibility for an issue. When the users array successfully prints, it does so in this format:
MyApp.User(id: "kugrbub", username: "user1", profileImageUrl: "https://theimageurl.com", email: "ceo#violent.jewelry", isCurrentUser: false),
When it prints, it's in parentheses, not braces. So is this a reason why it could be failing?

I believe I see what's going wrong. I think that you're misunderstanding what is happening with the asynchronous Firebase methods.
When you call a method like .getDocuments in Firebase, that method does not return instantaneously. Rather, it runs asynchronously and then calls your callback function that you provide when it's finished.
Why does this lead to the result that you're getting? In ContentView, you initialize SearchViewModel by calling SearchViewModel() to set up the property. In the init() of SearchViewModel, fetchUsers() is called, which populates the users array when it completes.
Then, once your login completes, ContentView switches over to your NavigationView/TabView, and onAppear is called. Inside, you call fetchUsers again, which starts running asynchronously. But, because you had already called fetchUsers in the SearchViewModel init, the array already has data in it (keep in mind that unless you explicitly sign out of Firebase, you'll still be signed in from the previous session).
So, how do you solve this?
Remember that all of those calls are asynchronous. Printing the arrays right after calling the fetchUsers method(s) will never get you the values you want *unless they had been populated before.
Restructure your app so that you can respond to the changes in the view model asynchronously. Because you have so much going on, there's not one definitive way to clean everything up. But, you can look into a couple of things:
look up onReceive which you can use to tell when a single publisher on your ObservableObject has changed.
When using Firebase, I like using Auth.auth().addStateDidChangeListener to respond to the auth state. That might help you start to split up your login logic.
Instead of printing after your fetchUsers call, print from inside the Firebase callback functions.
A code sample for you:
class SearchViewModel: ObservableObject {
#Published var users = [User]()
func fetchUsers() {
guard Auth.auth().currentUser?.uid != nil else {
assertionFailure("NOT SAFE TO GET USERS YET -- NOT LOGGED IN")
return
}
Firestore.firestore().collection("users").getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else {return}
print("Received docs:")
print(documents)
documents.forEach{ document in
let user = User(dictionary: document.data())
self.users.append(user)
}
print("USERS:")
print(self.users)
}
}
}
struct ShowUsersView: View {
#ObservedObject var searchViewModel = SearchViewModel()
var body: some View {
VStack {
ForEach(searchViewModel.users, id: \.id) { user in
Text(user.username)
}
}.onAppear {
print("Calling fetchUsers...")
searchViewModel.fetchUsers()
}
}
}

Related

How to use downloaded URL correctly in AsyncImage?

How to use downloaded URL from getData class in AsyncImage?
struct RecentItemsView: View {
var item: dataType // var's from getData class
var body: some View {
HStack(spacing: 15) {
AsyncImage(url: URL(string: item.pic), content: { image in // item.pic here
image.resizable()
}, placeholder: {
ProgressView()
})
I have full URL from downloadURL but when Im using item.pic parameter in AsyncImage I get error: (See Image)
I understand that the error contains the path to the image, which is not suitable for AsyncImage, that's why I downloaded the full URL, the question is how to use the received URL in AsyncImage?
class getData : ObservableObject {
#Published var datas = [dataType]()
init() {
let db = Firestore.firestore()
db.collection("items").getDocuments { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for i in snap!.documents {
let id = i.documentID
let title = i.get("title") as! String
let description = i.get("description") as! String
let pic = i.get("pic") as! String
self.datas.append(dataType(id: id, title: title, description: description, pic: pic))
let storage = Storage.storage()
let storageRef = storage.reference().child("\(pic)")
storageRef.downloadURL { url, error in
if let error = error {
print("Failed to download url:", error)
return
} else {
print(url!) // Full Url- https://firebasestorage.googleapis.com:...
}
}
}
}
}
}
struct dataType : Identifiable {
var id = UUID().uuidString
var title : String
var description : String
var pic : String
}
Error:
Storage:
Firestore:
This is going to look quite a bit different from your current approach but give it a try, it will simplify your code overall.
Main differences are the use of async await and FirebaseFirestoreSwift.
I choose using async await/Concurrency because it provides a more linear approach to the code and I think resolves your issue about sharing the variable with all the objects.
This is what your ObservableObject will look like
//Keeps UI Updates on the main thread
#MainActor
//Classes and structs should always be uppercased
class GetData : ObservableObject {
#Published var datas = [DataType]()
private var task: Task<Void, Never>? = nil
init() {
task = Task{
do{
try await getData()
}catch{
//Ideally you should present this error
//to the users so they know that something has gone wrong
print(error)
}
}
}
deinit{
task?.cancel()
}
func getData() async throws {
let documentPath = "items"
let svc = FirebaseService()
//async await allows a more linear approach. You can get the images individually
var items : [DataType] = try await svc.retrieve(path: documentPath)
for (idx, item) in items.enumerated() {
//Check if your url is a full url
if !item.pic.localizedCaseInsensitiveContains("https"){
//If it isnt a full url get it from storage and replace the url
items[idx].pic = try await svc.getImageURL(imagePath: item.pic).absoluteString
//Optional update the object so you dont have to retrieve the
//The url each time.
try svc.update(path: documentPath, object: items[idx])
}
}
datas = items
}
}
and your struct should change to use #DocumentID.
//This is a much simpler solution to decoding
struct DataType : Identifiable, FirestoreProtocol {
#DocumentID var id : String?
//If you get decoding errors make these variables optional by adding a ?
var title : String
var description : String
var pic : String
}
Your Views can now be modified to use the updated variables.
#available(iOS 15.0, *)
public struct DataTypeListView: View{
#StateObject var vm: GetData = .init()
public init(){}
public var body: some View{
List(vm.datas){ data in
DataTypeView(data: data)
}
}
}
#available(iOS 15.0, *)
struct DataTypeView: View{
let data: DataType
var body: some View{
HStack{
Text(data.title)
AsyncImage(url: URL(string: data.pic), content: { phase in
switch phase{
case .success(let image):
image
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
case .failure(let error):
Image(systemName: "rectangle.fill")
.onAppear(){
print(error)
}
case .empty:
Image(systemName: "rectangle.fill")
#unknown default:
Image(systemName: "rectangle.fill")
}
})
}
}
}
The class GetData is pretty bare bones an uses the code below to actually make the calls, I like using generics to simplify code and so it can be reused by various places.
You don't have to completely understand what is going on with this now but you should, I've put a ton of comments so it should be easy.
import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
import SwiftUI
import FirebaseAuth
struct FirebaseService{
private let storage: Storage = .storage()
private let db: Firestore = .firestore()
///Retrieves the storage URL for an image path
func getImageURL(imagePath: String?) async throws -> URL{
guard let imagePath = imagePath else {
throw AppError.unknown("Invalid Image Path")
}
typealias PostContinuation = CheckedContinuation<URL, Error>
//Converts an completion handler approach to async await/concurrency
return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
storage.reference().child(imagePath).downloadURL { url, error in
if let error = error {
continuation.resume(throwing: error)
} else if let url = url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AppError.unknown("Error getting image url"))
}
}
}
}
///Retireves the documetns from the Firestore and returns an array of objects
func retrieve<FC>(path: String) async throws -> [FC] where FC : FirestoreProtocol{
let snapshot = try await db.collection(path).getDocuments()
return snapshot.documents.compactMap { doc in
do{
return try doc.data(as: FC.self)
}catch{
//If you get any decoding errors adjust your struct, you will
//likely need optionals
print(error)
return nil
}
}
}
///Updates the provided document into the provided path
public func update<FC : FirestoreProtocol>(path: String, object: FC) throws{
guard let id = object.id else{
throw AppError.needValidId
}
try db.collection(path).document(id).setData(from: object)
}
}
enum AppError: LocalizedError{
case unknown(String)
case needValidId
}
protocol FirestoreProtocol: Identifiable, Codable{
///Use #DocumentID from FirestoreSwift
var id: String? {get set}
}
All of this code works, if you put all this code in a .swift file it will compile and it should work with your database.

Slight delay when retrieving from Firestore

When a user first logs into their profile, I retrieve there user name and profile picture. My issue is the site loads and firebase takes around a second to load the information. For example, there username will flash "unavailable" for a brief moment, before displaying the name.
Would love to get feedback on how to better improve my process of retrieving the information. Thank you! For the sake of less code, I didn't include my profile picture logic, as I'm guessing my issue has to do with the way I'm calling Firebase in the first place in my dashboard logic class.
struct UserDashController: View {
#ObservedObject var vm = DashboardLogic()
#State private var action: Int? = 0
#State private var userSigningOut = false
#State private var showMenu = false
#State private var presentSettingsPage = false
var body: some View {
NavigationView{
VStack{
HStack{
//retrieve username
Text(vm.userModel?.name ?? "Name Unavailable" )
}
.padding()
}
.padding(.top, -5)
}
}
Dashboard Logic
class DashboardLogic: ObservableObject {
#Published var userModel: UserModel?
#Published var privateUserModel: privateUserModel?
init(){
fetchCurrentUser()
}
private func fetchCurrentUser () {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
return
}
guard let email = FirebaseManager.shared.auth.currentUser?.email else {
print("could not locate email")
return
}
FirebaseManager.shared.firestore
.collection("users").document(uid)
.getDocument { snapshot, error in
if let error = error {
print ("failed to fetch user \(error)")
return
}
guard let data = snapshot?.data() else {
print ("no data found for user")
return
}
self.userModel = .init(data: data)
}
//save to private database
FirebaseManager.shared.firestore
.collection("users").document(uid)
.collection("privateUserInfo")
.document("private")
.getDocument { snapshot, error in
if let error = error {
print("oh no we messed up")
return
}
//save snapshot of database from firestore
guard let userEmail = snapshot?.data() else {
print("no email found for user")
return
}
self.privateUserModel = .init(data:userEmail )
}
}
}
USER MODEL
struct UserModel {
var uid, name, gender, height, weight, agenda, profilePictureURL: String
init(data: [String: Any]){
self.uid = data["uid"] as? String ?? "Unavailable"
self.name = data["name"] as? String ?? "Unavailable"
self.gender = data["gender"] as? String ?? "Unavailable"
self.height = data["height"] as? String ?? "Unavailable"
self.weight = data["weight"] as? String ?? "Unavailable"
self.agenda = data["agenda"] as? String ?? "Unavailable"
self.profilePictureURL = data ["profilePicture"] as? String ?? "Unavailable"
}
}
struct privateUserModel {
var email: String
init(data: [String: Any]){
self.email = data["email"] as? String ?? "Unavailable"
}
}
The only I would change is to update published properties on main queue, like
DispatchQueue.main.async {
self.userModel = .init(data: data)
}
// ...
DispatchQueue.main.async {
self.privateUserModel = .init(data:userEmail )
}
everything else I assume is just network delays.
Update
It’s not predictable for how long the latency will last, but you could implement loading animation features while waiting for the data to be fetched from Cloud Firestore instead of having just a simple “Unavailable” message, like in this post.
There are also some ways to handle loading states within SwiftUI views as described here, from self-loading views, view models to connecting Combine publishers to views and supporting custom loading views.
On the other hand, you could use an activity indicator and try a view called ProgressView as described in this other post.
As a complement and from a Cloud Firestore configuration perspective, you can improve your process of retrieving data following the best practices such as selecting the database location closest to your users and compute resources. Far-reaching network hops are more error-prone and increase query latency.
You can also follow these practices about the use of asynchronous methods that can reduce your delays as completion handlers or using async/await.
Finally, consider that the delay can be generated for external conditions in the network itself.
You may also be interested in looking into this other related question.

Swiftui - Fetch data after login

I'm starting to learn SwiftUI and i'm looking for better solution to fetch user data after successful login.
For example, i need to see if the user has the account blocked or not after login
For this i have create struct in Users.swift
struct Users: Identifiable, Codable{
#DocumentID var id: String?
var Societe : String
var Nom : String
var Prenom : String
var HasBlocked : Bool
var SiteID : [String]
var Poste : String
var Email : String
}
and firebase class in Firebase.swift
class Firebase: ObservableObject {
#EnvironmentObject var viewRouter: ViewRouter
#Published var user : Users? = nil
private var db = Firestore.firestore()
func fetchData() {
db.collection("Users").document(Auth.auth().currentUser!.uid).addSnapshotListener{ (snapshot, error) in
guard let documents = snapshot?.data() else {
print("No documents")
return
}
let name = documents["Nom"] as? String ?? ""
let surname = documents["Prenom"] as? String ?? ""
let email = documents["Email"] as? String ?? ""
let societe = documents["Societe"] as? String ?? ""
let poste = documents["Poste"] as? String ?? ""
let siteId = documents["SiteID"] as? [String] ?? []
let hasBlocked = documents["HasBlocked"] as? Bool ?? true
if let user = self.user?.SiteID{
print(user)
}else{
print("no document2")
}
self.user = Users(Societe: societe, Nom: name, Prenom: surname, HasBlocked: hasBlocked, SiteID: siteId, Poste: poste, Email: email)
}
}
}
and in the SignInView after press login button, i call func signInUser
#ObservedObject private var firebase = Firebase()
func signInUser(userEmail: String, userPassword: String) {
signInProcessing = true
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
guard error == nil else {
signInProcessing = false
signInErrorMessage = error!.localizedDescription
return
}
switch authResult {
case .none:
print("Could not sign in user.")
signInProcessing = false
case .some(_):
print("User signed in")
// get data and check ??
signInProcessing = false
withAnimation {
viewRouter.currentPage = .homePage
}
}
}
}
But i don't know how to fetch data after login for check if the account has blocked and then pass the data to Homeview without re-fetch data to optimize call database.
thank for your help
Let me address this at a high level as your code seems fine but its's just wrapping your brain around the asynchronous calls.
I am pretty sure you have everything you need - here's the flow using pseudo code
authenticate { auth closure
read user document using auth.uid { snapshot closure
if snapshot has user blocked {
do not proceed to next view
} else { //user is not blocked
proceed to next view
}
}
}
Remember that Firebase data is only valid within the closure following the Firebase call and if you follow the above pseudo code, it does just that...
auth closure - has valid firebase auth information so you can get the uid
snapshot closure - has valid snapshot info so you can see if the user is blocked
the If statement will then determine the next thing to do - either don't show
the view if blocked or show it if not blocked
So borrowing from your code
func signInUser(userEmail: String, userPassword: String) {
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
db.collection("Users").document(Auth.auth().currentUser!.uid).getDocument {
either go to next view or not
Note that you should be using getDocument in this case to read the document once instead of adding a listener.

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

How to grab the current users "firstname" from firebase store. Swift 5

I did more trial and error and a bit of online research and this is what I came back with:
func presentWelcomeMessage() {
//Get specific document from current user
let docRef = Firestore.firestore()
.collection("users")
.whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
// Get data
docRef.getDocuments { (querySnapshot, err) in
if let err = err {
print(err.localizedDescription)
} else if querySnapshot!.documents.count != 1 {
print("More than one document or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
guard let firstname = dataDescription?["firstname"] else { return }
self.welcomeLabel.text = "Hey, \(firstname) welcome!"
}
}
It works, but am not sure if it is the most optimal solution.
First I should say firstname is not really the best way to store a var. I would recommend using firstName instead for readability. I also recommend getting single documents like I am, rather than using a whereField.
An important thing to note is you should create a data model like I have that can hold all of the information you get.
Here is a full structure of how I would get the data, display it, and hold it.
struct UserModel: Identifiable, Codable {
var id: String
var firstName: String
private enum CodingKeys: String, CodingKey {
case id
case firstName
}
}
import SwiftUI
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
class UserDataManager: ObservableObject {
private lazy var authRef = Auth.auth()
private lazy var userInfoCollection = Firestore.firestore().collection("users")
public func getCurrentUIDData(completion: #escaping (_ currentUserData: UserModel) -> Void) {
if let currentUID = self.authRef.currentUser?.uid {
self.userInfoCollection.document(currentUID).getDocument { (document, error) in
if let document = document {
if let userData = try? document.data(as: UserModel.self) {
completion(userData)
}
} else if let error = error {
print("Error getting current UID data: \(error)")
}
}
} else {
print("No current UID")
}
}
}
struct ContentView: View {
#State private var userData: UserModel? = nil
private let
var body: some View {
ZStack {
if let userData = self.userData { <-- safely unwrap data
Text("Hey, \(userData.firstName) welcome!")
}
}
.onAppear {
if self.userData == nil { <-- onAppear can call more than once
self.udm.getCurrentUIDData { userData in
self.userData = userData <-- pass data from func to view
}
}
}
}
}
Hopefully this can point you in a better direction of how you should be getting and displaying data. Let me know if you have any further questions or issues.