I cant present my view with foreach in SwiftUI - google-cloud-firestore

I try to present a View-struct as often as there are items in an array. I want to use for-each, because I don't like the UIList view. Btw I'm using SwiftUI. I generate the array which I want to use from firebase-firestore.
Here is how I generate my array:
class ViewModellForItems: ObservableObject{
#Published var listItemsEnglisch = [MaterialItemClass]()
let myDataBase = Firestore.firestore()
let Ordner = Firestore.firestore().collection("Censored")
func updateData(){
Ordner.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("Ordner wurde nicht gefunden")
return
}
self.listItemsMathe = documents.map { (queryDocumentSnapshot) -> MaterialItemClass in
let data = queryDocumentSnapshot.data()
let Name = data["Name"] as? String ?? ""
let beschreibung = data["beschreibung"] as? String ?? ""
let anzahlDesProduktes = data["anzahlDesProduktes"] as? Int ?? 0
let bildName = data["bildName"] as? String ?? ""
let hintergrundFarbe = data["hintergrundFarbe"] as? String ?? ""
let item = MaterialItemClass(Name: Name, beschreibung: beschreibung, anzahlDesProduktes: anzahlDesProduktes, bildName: bildName, hintergrundFarbe: hintergrundFarbe)
return item
}
}
}
}
Here is the Struct that I use in the ViewModellForItems Class:
struct MaterialItemClass {
var Name: String
var beschreibung: String
var anzahlDesProduktes: Int
var bildName: String
var hintergrundFarbe: String
}
And here is my ContendView.swift File:
struct ContendView: View {
#ObservedObject private var viewModel = ViewModellForItems()
var body: some View {
ForEach(0 ..< viewModel.listItemsEnglisch.count, id: \.self) {
Text(viewModel.listItemsEnglisch[$0].Name)
}.onAppear(){
self.viewModel.updateData()
}
Text("Debug")
}
}
I only get presented the Debug-Text... what am I doing wrong? And further; how can I present a whole View-Struct for each element I this array?
Just want to say, there's no fail of the firebase, because if I run almost the same code in a list view, everything is working fine...

Ok boys I got my mistake,
if I update my Array only in the .onAppear Methode of my for each block, it won't update, because the for-each block will never appear.
Thanks for your time
Boothosh

Related

Function with a parameter inside init() not updating onAppear{}

Attempting to call a function within init(). I have a function that takes a parameter which I call in the initializer. I set the parameter to a blank string at first until I pass in the value in another view when it appears.
My issue is the function isn't updating immediately when the view first appears. My objective is to just have the function run immediately once the view is generated and have the view update immediately (where I display in my view values from the function).
My guess is since I'm passing in the initial blank string during the init(), my function isn't firing with the updated variable. I don't want to set the #State to #Binding as I don't want to have to pass in a value everytime I call the observedObject. Any advice is greatly appreciated.
To summarize my issue, when I call the grabItems recipe when my view first appears, it doesn't initially get called with the correct parameter (it gets called with a blank string).
class testClass: ObservableObject {
#State var uidNonUser: String
#Published var itemsNonUser = [Item]() // items saved
init(){
self.uidNonUser = ""
grabItems(userUID: uidNonUser) // << grabs things
}
}
struct testStruct: View {
#ObservedObject var rm = testClass()
#State var userUID: String // << variable passing to grabItems
var body: some View {
Text(rm.itemsNonUser.count)
.onAppear{
rm.grabItems(userUID: userUID)
}
}
}
FYI - pasting my actual grabItems recipe below just as reference in case it helps understand the issue.
func grabItems(userUID: String){
//grab user thats not current user
FirebaseManager.shared.firestore
.collection("users")
.document(userUID) // << passed in from view
.collection("userRecipes")
.addSnapshotListener { (snapshot, err) in
guard let documents = snapshot?.documents else{
print("no documents present")
return
}
self.itemsNonUser = documents.map { (querySnapshot) -> RecipeItem in
let data = querySnapshot.data()
let recipeTitle = data ["recipeTitle"] as? String ?? ""
let recipePrepTime = data ["recipePrepTime"] as? String ?? ""
let recipeImage = data ["recipeImage"] as? String ?? ""
let createdAt = data ["createdAt"] as? String ?? ""
let ingredients = data ["ingredientItem"] as? [String: String] ?? ["": ""]
let directions = data ["directions"] as? [String] ?? [""]
let recipeID = data ["recipeID"] as? String ?? ""
let recipeCaloriesMacro = data ["recipeCaloriesMacro"] as? Int ?? 0
let recipeFatMacro = data ["recipeFatMacro"] as? Int ?? 0
let recipeCarbMacro = data ["recipeCarbMacro"] as? Int ?? 0
let recipeProteinMacro = data ["recipeProteinMacro"] as? Int ?? 0
let recipe = RecipeItem(id: recipeID, recipeTitle:recipeTitle , recipePrepTime: recipePrepTime, recipeImage: recipeImage, createdAt: createdAt, recipeCaloriesMacro: recipeCaloriesMacro, recipeFatMacro: recipeFatMacro, recipeCarbMacro:recipeCarbMacro, recipeProteinMacro: recipeProteinMacro, directions: directions, ingredientItem: ingredients)
return recipe
}
}
}
Try this example code to update the View when it first appears.
// for testing
struct RecipeItem {
var recipeTitle: String
// ...
}
class TestClass: ObservableObject {
#Published var itemsNonUser = [RecipeItem]() // <-- here
func grabItems(userUID: String){
//...
// for testing
itemsNonUser = [RecipeItem(recipeTitle: "banana cake with ID: \(userUID)")]
}
}
struct TestStruct: View {
#StateObject var rm = TestClass() // <-- here
#State var userUID: String
var body: some View {
VStack {
Text(rm.itemsNonUser.first?.recipeTitle ?? "no name")
Text("count: \(rm.itemsNonUser.count)") // <-- here
}
.onAppear{
rm.grabItems(userUID: userUID) // <-- here
}
}
}
struct ContentView: View {
let userUID = "123456"
var body: some View {
TestStruct(userUID: userUID) // <-- here
}
}
In my instance (and could have been specific for my particular case) I updated the grabItems recipe to return an Int since my view was trying to display the count.
I then just called it directly into the view like so and it worked:
Text(String(rm.grabItems(userUID: userUID))).bold() // << calling function directly in view

Fails to update UI after firestore listener fires

I am trying to update my UI each time there have been changes in firestore document.
I when I check with console, I see that the listener fires each time I change document.
My listener and 'readyOrders' is #Published:
func getReadyOrders() {
referance
.collection(path)
.document(email)
.collection("CompletedOrders")
.whereField("placedBy", isEqualTo: user)
.addSnapshotListener { orderSnapshot, error in
DispatchQueue.main.async {
guard let snapshot = orderSnapshot?.documents else{
print("There is no active orders")
return
}
self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
let data = activeSnapshot.data()
var collectedItems = [MenuItem]()
var collectedDrinks = [DrinkItem]()
let id = activeSnapshot.documentID
let placed = data["placedBy"] as? String ?? ""
let inZone = data["inZone"] as? String ?? ""
let forTable = data["forTable"] as? String ?? ""
let orderItems = data["orderItems"] as? [String]
let orderDrinks = data["orderDrinks"] as? [String]
let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
let totalAmount = data["totalAmount"] as? Double ?? 0.00
orderItems?.forEach({ item in
let parts = item.components(separatedBy: "/")
collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
})
orderDrinks?.forEach({ drink in
let itemPart = drink.components(separatedBy: "/")
collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
})
return ActiveOrder(id: id,
placedBy: placed,
inZone: inZone,
forTable: forTable,
orderItems: collectedItems,
orderDrinks: collectedDrinks,
orderItemsReady: orderItemsReady,
orderDrinksReady: orderDrinksReady,
totalAmount: totalAmount)
}
}
}
}
View where I display all the documents
Note: This UI is updating when there is added new document or deleted current one.
Section {
ForEach(handler.readyOrders, id: \.id){ readyOrder in
NavigationLink{
OrderComplete(handler: handler, order: readyOrder, currency: currency)
} label: {
HStack{
Text(readyOrder.inZone!)
Text("- \(readyOrder.forTable!)")
}
}
}
} header: {
Text("Order's ready:")
}
And in this view I display the content of document, right in here the view does not update. To the file where I am displaying content I pass in the readyOrder from 'ForEach' and there I take the array in ready order and display it in 'ForEach':
ForEach(order.orderItems!, id:\.id){ item in
HStack{
Text(item.itemName!)
Spacer()
Text(currency.format(item.price!))
.foregroundColor(.teal)
Image(systemName: "arrow.left")
.foregroundColor(.red)
}
}
.onDelete(perform: deleteItem)
I have tried many things, and I am sure there is a simple solution, that I dont quite get. Because I am new to SwiftUI.
Edit:
I have puted together the the code for minimal repruduction as requested so there would more context for what I am trying to do.
Model:
struct Order: Identifiable{
var id = UUID().uuidString
var placedBy: String?
var inZone: String?
var forTable: String?
var orderItems: [MenuItem]?
var orderDrinks: [DrinkItem]?
var orderItemsReady: Bool?
var orderDrinksReady: Bool?
var totalAmount: Double?}
ViewModel:
class ViewModel: ObservableObject{
#Published var orders = [Order]()
private var referance = Firestore.firestore()
func getReadyOrders() {
referance
.collection(path)
.document(email)
.collection("CompletedOrders")
.whereField("placedBy", isEqualTo: user)
.addSnapshotListener { orderSnapshot, error in
DispatchQueue.main.async {
guard let snapshot = orderSnapshot?.documents else{
print("There is no active orders")
return
}
self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
let data = activeSnapshot.data()
var collectedItems = [MenuItem]()
var collectedDrinks = [DrinkItem]()
let id = activeSnapshot.documentID
let placed = data["placedBy"] as? String ?? ""
let inZone = data["inZone"] as? String ?? ""
let forTable = data["forTable"] as? String ?? ""
let orderItems = data["orderItems"] as? [String]
let orderDrinks = data["orderDrinks"] as? [String]
let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
let totalAmount = data["totalAmount"] as? Double ?? 0.00
orderItems?.forEach({ item in
let parts = item.components(separatedBy: "/")
collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
})
orderDrinks?.forEach({ drink in
let itemPart = drink.components(separatedBy: "/")
collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
})
return ActiveOrder(id: id,
placedBy: placed,
inZone: inZone,
forTable: forTable,
orderItems: collectedItems,
orderDrinks: collectedDrinks,
orderItemsReady: orderItemsReady,
orderDrinksReady: orderDrinksReady,
totalAmount: totalAmount)
}
}
}
}
func delteItem(menuItem: MenuItem, from order: ActiveOrder){
let item = menuItem.itemName! + "/" + String(menuItem.price!)
let pathTo = referance.collection(path).document(email).collection("CompletedOrders").document(order.id)
pathTo.getDocument { snapshot, error in
if let document = snapshot, document.exists{
var items = document.data()!["orderItems"] as? [String] ?? []
let index = items.firstIndex(where: { $0 == item })
items.remove(at: index!)
pathTo.updateData(["orderItems" : items]){ error in
if let _ = error{
print("Error deleting and updating order array")
}
}
}
}}}
And the veiws:
struct View1: View{
#ObservedObject var viewModel: ViewModel
var body: some View{
VStack{
ForEach(viewModel.orders, id: \.id){ readyOrder in
NavigationLink{
View2(viewModel: viewModel, order: readyOrder)
} label: {
HStack{
Text(readyOrder.inZone!)
Text("- \(readyOrder.forTable!)")
}
}
}
}
}}
struct View2: View{
#ObservedObject var viewModel: ViewModel
var order: Order
func deleteItem(at offest: IndexSet){
let index = offest[offest.startIndex]
let deleteItem = order.orderItems![index]
handler.delteItem(menuItem: deleteItem, from: order)
}
//In this view I want to get updated elements from document to display -> or if removed.
var body: some View{
VStack{
ForEach(order.orderItems!, id:\.id){ item in
HStack{
Text(item.itemName!)
Spacer()
Text(item.price!)
.foregroundColor(.teal)
Image(systemName: "arrow.left")
.foregroundColor(.red)
}
}
.onDelete(perform: deleteItem)
}
}}
SOLVED
I literally dont know why but when I changed my refarance from:
#ObservedObject var viewModel: Viewmodel
//To the stateobject
#StateObject var viewModel: ViewModel
As I understand the #StateObject in swift is used first time initializing view model class, and then you should use #ObservedObject as passing the view model further. If any body could explain me why in this case the state object worked it would be nice.

Thread 1: EXC_BAD_INSTRUCTION when fetching data

I get this Error -> Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) randomly. I don't quite understand when exactly it happens. Most of the times it is when the view refreshes. The Error appears at the line where group.leave() gets executed.
What am I trying to do:
I want to fetch albums with their image, name and songs that also have a name and image from my firebase database. I checked for the values and they're all right as far as I can tell. But when trying to show them it is random what shows. Sometimes everything is right, sometimes one album gets showed twice, sometimes only one album gets showed at all, sometimes one album has the songs of the other album.
My firebase database has albums stored as documents, each document has albumimage/name and 2 subcollections of "unlocked" with documents(user uid) that store "locked":Bool and "songs" with a document for each song that stores image/name
This is the function that fetches my albums with their songs:
let group = DispatchGroup()
#State var albums: [Album] = []
#State var albumSongs: [AlbumSong] = []
func fetchAlbums() {
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
guard let documents = querySnapshot?.documents else {
return
}
let uid = FirebaseManager.shared.auth.currentUser?.uid
documents.forEach { document in
let data = document.data()
let name = data["name"] as? String ?? ""
let artist = data["artist"] as? String ?? ""
let releaseDate = data["releaseDate"] as? Date ?? Date()
let price = data["price"] as? Int ?? 0
let albumImageUrl = data["albumImageUrl"] as? String ?? ""
let docID = document.documentID
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
if let error = error {
return
}
guard let documents = querySnapshot?.documents else {
return
}
self.albumSongs = documents.compactMap { document -> AlbumSong? in
do {
return try document.data(as: AlbumSong.self)
} catch {
return nil
}
}
group.leave()
}
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
if let error = error {
return
}
guard let document = docSnapshot?.data() else {
return
}
group.enter()
group.notify(queue: DispatchQueue.global()) {
if document["locked"] as! Bool == true {
self.albums.append(Album(name: name, artist: artist,
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
print("albums: ",albums)
}
}
}
}
}
}
I call my fetchAlbums() in my view .onAppear()
My AlbumSong:
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
My Album:
struct Album: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let artist: String
let songs: [AlbumSong]
let releaseDate: Date
let price: Int
let albumImageUrl: String
}
I tried looking into how to fetch data from firebase with async function but I couldn't get my code to work and using dispatchGroup worked fine when I only have one album. I would appreciate answers explaining how this code would work with async, I really tried my best figuring it out by myself a long time. Also I would love to know what exactly is happening with DispatchGroup and why it works fine having one album but not with multiple ones.
I think you are over complicating something that is very simple with async await
First, your Models need some adjusting, it may be the source of some of your issues.
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct AlbumSong: Identifiable, Codable {
//No need to set a UUID `#DocumentID` provides an ID
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
struct Album: Identifiable, Codable {
//No need to set a UUID `#DocumentID` provides an ID
#DocumentID var id: String?
let name: String
let artist: String
//Change to var and make nil, the initial decoding will be blank
//If any of the other variables might be optional add the question mark
var songs: [AlbumSong]?
let releaseDate: Date
let price: Int
let albumImageUrl: String
}
Then you can create a service that does the heavy lifting with the Firestore.
struct NestedFirestoreService{
private let store : Firestore = .firestore()
let ALBUM_PATH = "albums"
let SONG_PATH = "songs"
///Retrieves Albums and Songs
func retrieveAlbums() async throws -> [Album] {
//Get the albums
var albums: [Album] = try await retrieve(path: ALBUM_PATH)
//Get the songs, **NOTE: leaving the array of songs instead of making a separate collection might work best.
for (idx, album) in albums.enumerated() {
if let id = album.id{
albums[idx].songs = try await retrieve(path: "\(ALBUM_PATH)/\(id)/\(SONG_PATH)")
}else{
print("\(album) :: has invalid id")
}
}
//Add another loop for `unlocked` here just like the one above.
return albums
}
///retrieves all the documents in the collection at the path
public func retrieve<FC : Identifiable & Codable>(path: String) async throws -> [FC]{
let querySnapshot = try await store.collection(path)
.getDocuments()
return try querySnapshot.documents.compactMap { document in
try document.data(as: FC.self)
}
}
}
Then you can implement it with just a few lines in your presentation layer.
import SwiftUI
#MainActor
class AlbumListViewModel: ObservableObject{
#Published var albums: [Album] = []
private let svc = NestedFirestoreService()
func loadAlbums() async throws{
albums = try await svc.retrieveAlbums()
}
}
struct AlbumListView: View {
#StateObject var vm: AlbumListViewModel = .init()
var body: some View {
List(vm.albums, id:\.id) { album in
DisclosureGroup(album.name) {
ForEach(album.songs ?? [], id:\.id){ song in
Text(song.title)
}
}
}.task {
do{
try await vm.loadAlbums()
}catch{
print(error)
}
}
}
}
struct AlbumListView_Previews: PreviewProvider {
static var previews: some View {
AlbumListView()
}
}
If you get any decoding errors make the variables optional by adding the question mark to the type like I did with the array.
Just use them in the correct order:
let group = DispatchGroup()
#State var albums: [Album] = []
#State var albumSongs: [AlbumSong] = []
func fetchAlbums() {
group.enter()
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
group.leave()
return
}
guard let documents = querySnapshot?.documents else {
group.leave()
return
}
let uid = FirebaseManager.shared.auth.currentUser?.uid
documents.forEach { document in
let data = document.data()
let name = data["name"] as? String ?? ""
let artist = data["artist"] as? String ?? ""
let releaseDate = data["releaseDate"] as? Date ?? Date()
let price = data["price"] as? Int ?? 0
let albumImageUrl = data["albumImageUrl"] as? String ?? ""
let docID = document.documentID
group.enter()
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
if let error = error {
group.leave()
return
}
guard let documents = querySnapshot?.documents else {
group.leave()
return
}
self.albumSongs = documents.compactMap { document -> AlbumSong? in
do {
group.leave()
return try document.data(as: AlbumSong.self)
} catch {
group.leave()
return nil
}
}
}
group.enter()
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
if let error = error {
group.leave()
return
}
guard let document = docSnapshot?.data() else {
group.leave()
return
}
if document["locked"] as! Bool == true {
self.albums.append(Album(name: name, artist: artist,
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
print("albums: ",albums)
}
group.leave()
}
}
group.leave()
}
group.notify(queue: DispatchQueue.global()) {
// do your stuff
}
}

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.

Firestore responding with "cannot find 'cards' in scope"

I followed this tutorial to get data from firestore and changed what i needed to correspond to my model but it keeps responding with "cannot find 'cards' in scope" and I'm not sure what i did wrong. (i think i got the mvvm labels right)
VIEW
import SwiftUI
struct TestingView: View {
#ObservedObject private var viewModel = CardViewModel()
var body: some View {
List(viewModel.cards) {
Text(cards.name)
}
.onAppear() {
self.viewModel.fetchData()
}
}
}
VIEW MODEL
import Foundation
import Firebase
class CardViewModel: ObservableObject {
#Published var cards = [Cards]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("cards").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.cards = documents.map { queryDocumentSnapshot -> Cards in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let pronoun = data["pronoun"] as? String ?? ""
let bio = data["bio"] as? String ?? ""
let profileURLString = data["profileURLString"] as? String ?? ""
let gradiantColor1 = data["gradiantColor1"] as? UInt ?? 0
let gradiantColor2 = data["gradiantColor2"] as? UInt ?? 0
let gradiantColor3 = data["gradiantColor3"] as? UInt ?? 0
return Cards(name: name, pronoun: pronoun, bio: bio, profileURLString: profileURLString, gradiantColor1: gradiantColor1, gradiantColor2: gradiantColor2, gradiantColor3: gradiantColor3)
}
}
}
}
MODEL
import Foundation
struct Cards: Identifiable {
var id = UUID().uuidString
var name: String
var pronoun: String
var bio: String
var profileURLString: String
var gradiantColor1: UInt
var gradiantColor2: UInt
var gradiantColor3: UInt
var profileURL: URL {
return URL(string: profileURLString)!
}
}
List will provide an element to its trailing closure -- see card in in my code. Then, you can access that specific card in your Text element.
var body: some View {
List(viewModel.cards) { card in //<-- Here
Text(card.name) //<-- Here
}
.onAppear() {
self.viewModel.fetchData()
}
}
}
I'd suggest that you might want to rename the struct Cards to struct Card since it is one card. Then, your array would be #Published var cards = [Card]() -- ie an array of Cards. From a naming perspective, this would make a lot more sense.