Firestore not returning data to #Published variable - swift

I have a call to Firestore that grabs some data and stores to
#published var userJournals
In another view, I go to call the variable and it's empty. I checked the data in the initial pull from firestore and the data shows and is mapped successfully. Wondering what I'm doing wrong on the other view.
View 1
class JournalDashLogic: ObservableObject {
#Published var userJournals = [UserJournalEntry]()
#Published var userJournalIDs = [String]()
func grabUserJournals(journalID: String) {
//grab current user
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
return
}
FirebaseManager.shared.firestore
.collection("users")
.document(uid)
.collection("userJournalEntrys")
.document(journalID)
.collection("items")
.addSnapshotListener { (snapshot, err) in
guard let documents = snapshot?.documents else {
print("no documents present")
return
}
//map to journal entry struct
self.userJournals = documents.map { (querySnapshot) -> UserJournalEntry in
let data = querySnapshot.data()
let dateCreated = data["dateCreated"] as? String ?? ""
let dayOfWeek = data["dayOfWeek"] as? String ?? ""
let mealCalories = data["mealCalories"] as? Int ?? 0
let mealCarbs = data["mealCarbs"] as? Int ?? 0
let mealFat = data["mealFat"] as? Int ?? 0
let mealName = data["mealName"] as? String ?? ""
let mealProtein = data["mealProtein"] as? Int ?? 0
let MealServing = data["MealServing"] as? Int ?? 0
let mealSaved = data["saved"] as? Bool ?? false
let mealTiming = data["timeCreated"] as? String ?? ""
let entry = UserJournalEntry(
id: UUID().uuidString, mealName: mealName, mealFat: mealFat, mealCarbs: mealCarbs,
mealProtein: mealProtein, mealCalories: mealCalories, MealServing: MealServing,
mealSaved: mealSaved, mealTiming: mealTiming, dayOfWeek: dayOfWeek,
totalCalories: "100", dateCreated: dateCreated)
return entry
}
}
}
}
View 2
.onAppear {
for id in jm.userJournalIDs {
jm.grabUserJournals(journalID: id)
}
}
sheet presented from View 2
.sheet(isPresented: $showAllJournals) {
SavedJournalsList(savedJournalIDs: jm.userJournalIDs, savedJournals: jm.userJournals)
.transition(transition)
}
View 3
struct SavedJournalsList: View {
#State var savedJournalIDs: [String]
#State var savedJournals: [UserJournalEntry]
var body: some View {
VStack(alignment: .leading) {
ForEach(savedJournals, id: \.self) { entry in
HStack {
Text(entry.dateCreated).bold()
Text("Total Calories: 3200")
.padding(.leading, 15)
}
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
HStack {
Text("200 Carbs")
.foregroundColor(.gray)
Text("250 Protein")
.foregroundColor(.gray)
Text("100 Fat")
.foregroundColor(.gray)
}
.padding(.all, 5)
.frame(maxWidth: .infinity)
}
.frame(width: 300, height: 100)
.background(.white)
.cornerRadius(15)
.shadow(color: Color("LighterGray"), radius: 5, x: 0, y: 8)
}
}
}

You can try this function which i have created for my project
func lisentDocument<T:Codable>(docPath: String, model: T.Type, completion:#escaping(T?,String?) -> Void) -> ListenerRegistration? {
let docRef = db.document(docPath)
var listner: ListenerRegistration? = nil
listner = docRef.addSnapshotListener { response, error in
if error != nil {
completion(nil, error?.localizedDescription)
print(listner)
}
do {
let data = try response?.data(as: model)
completion(data, nil)
} catch {
completion(nil, FBError.someThingWentWrong.message)
}
}
return listner
}
How to use
lisentDocument(docPath: "Doc Path", model: [UserJournalEntry].self,
completion: { data, error in
self.userJournals = data
})
If you have Object as an response use can use model as UserJournalEntry.self

A couple of notes:
MealServing is in uppercase, I think it should probably be lowercase: mealServing.
No need to map data manually, use Codable instead. See the official docs
you're using a view model, so no need to use #State properties on your vies. Use the view models instead.
You're iterating over the IDs in your view model and it seems like you're trying to set up a snapshot listener for each of those documents. This is inefficient, and you should instead set up a listener on the collection (or a query), and then assign the result of this to the published property userJournals. The documentation has an example that shows how to do this here. If you're interested in the code sample, check out this repo

Related

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.

Running into the Error: keyNotFound, DecodingError when trying to fetch messages from Firebase Database

I get this Error:
keyNotFound(CodingKeys(stringValue: "email", intValue: nil),
Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with
key CodingKeys(stringValue: \"email\", intValue: nil) (\"email\").", underlyingError: nil))
I am not sure what this Error is trying to tell me, I read through this question: Swift Error- keyNotFound(CodingKeys(stringValue:, intValue: nil), Swift.DecodingError.Context, which has the same Error. Though I couldn't figure out how to change my ChatUser so that my App works.
Trying to fetch messages from my Firestore database.
I tried debugging and I am pretty sure
ForEach(vm.recentMessages) { recentMessage in } leads to the error.
This is my ViewModel:
class MessagesViewModel: ObservableObject {
#Published var errorMessage = ""
#Published var chatUser: ChatUser?
init() {
fetchCurrentUser()
fetchRecentMessages()
}
#Published var recentMessages = [RecentMessage]()
private var firestoreListener: ListenerRegistration?
func fetchRecentMessages() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else { return }
firestoreListener?.remove()
self.recentMessages.removeAll()
firestoreListener = FirebaseManager.shared.firestore
.collection(FirebaseConstants.recentMessages)
.document(uid)
.collection(FirebaseConstants.messages)
.order(by: FirebaseConstants.timestamp)
.addSnapshotListener { querySnapshot, error in
if let error = error {
self.errorMessage = "Failed to listen for recent messages: \(error)"
print(error)
return
}
querySnapshot?.documentChanges.forEach({ change in
let docId = change.document.documentID
if let index = self.recentMessages.firstIndex(where: { rm in
return rm.id == docId
}) {
self.recentMessages.remove(at: index)
}
do {
let rm = try change.document.data(as: RecentMessage.self)
self.recentMessages.insert(rm, at: 0)
} catch {
print(error)
}
})
}
}
func fetchCurrentUser() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
self.errorMessage = "Could not find firebase 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
}
self.chatUser = try? snapshot?.data(as: ChatUser.self)
FirebaseManager.shared.currentUser = self.chatUser
}
}
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
}
self.chatUser = try? snapshot?.data(as: ChatUser.self)
FirebaseManager.shared.currentUser = self.chatUser
}
}
}
This is the View the messages should appear in:
struct MessagesView: View {
#ObservedObject private var vm = MessagesViewModel()
private var chatLogViewModel = ChatLogViewModel(chatUser: FirebaseManager.shared.currentUser)
#State var chatUser: ChatUser?
var body: some View {
VStack {
HStack {
Button() {
} label: {
Image("search").resizable()
.frame(width: 32, height: 32)
.padding(.leading, 11)
}
Spacer()
Image("AppIcon").resizable()
.frame(width: 32, height: 32)
.scaledToFill()
Spacer()
Button() {
} label: {
Image("dots").resizable()
.renderingMode(.template)
.frame(width: 32, height: 32)
.foregroundColor(Color(.init(red: 0.59, green: 0.62, blue: 0.67, alpha: 1)))
.padding(.trailing, 9)
.offset(y: -4)
}
}.padding(.init(top: 0, leading: 8, bottom: 0, trailing: 8))
NavigationView {
VStack {
messagesView
}
}
}
}
private var messagesView: some View {
ScrollView {
ForEach(vm.recentMessages) { recentMessage in
VStack {
Button {
let uid = FirebaseManager.shared.auth.currentUser?.uid == recentMessage.fromId ? recentMessage.toId : recentMessage.fromId
self.chatUser = .init(id: uid, uid: uid, email: recentMessage.email, profileImageUrl: recentMessage.profileImageUrl)
self.chatLogViewModel.chatUser = self.chatUser
self.chatLogViewModel.fetchMessages()
} label: {
HStack(spacing: 16) {
WebImage(url: URL(string: recentMessage.profileImageUrl))
.resizable()
.scaledToFill()
.frame(width: 64, height: 64)
.clipped()
.cornerRadius(64)
.overlay(RoundedRectangle(cornerRadius: 64)
.stroke(Color.black, lineWidth: 1))
.shadow(radius: 5)
VStack(alignment: .leading, spacing: 8) {
Text(recentMessage.username)
.font(.system(size: 16, weight: .bold))
.foregroundColor(Color(.label))
.multilineTextAlignment(.leading)
Text(recentMessage.text)
.font(.system(size: 14))
.foregroundColor(Color(.darkGray))
.multilineTextAlignment(.leading)
}
Spacer()
Text(recentMessage.timeAgo)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Color(.label))
}
}
Divider()
.padding(.vertical, 8)
}.padding(.horizontal)
}.padding(.bottom, 50)
}
}
}
ChatUser looks like this:
import FirebaseFirestoreSwift
struct ChatUser: Codable, Identifiable {
#DocumentID var id: String?
let uid, email, profileImageUrl: String
}
struct RecentMessage: Codable, Identifiable {
#DocumentID var id: String?
let text, email: String
let fromId, toId: String
let profileImageUrl: String
let timestamp: Date
var username: String {
email.components(separatedBy: "#").first ?? email
}
var timeAgo: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: timestamp, relativeTo: Date())
}
}
Update
I made some adjustments and added the RecentMessage portion. Now it works entirely for me. I can get the current (ChatUser) from Firestore and I also can get the RecentMessages. Now it loads all RecentMessages, this should be changes to ones that are in relationship with the current user.
Models
ChatUser
import Foundation
import SwiftUI
import Firebase
struct ChatUser: Codable, Identifiable, Hashable {
var id: String?
var email: String
var profileImageUrl: String
var uid: String
init(email: String, profileImageUrl: String, uid: String, id: String?) {
self.id = id
self.email = email
self.profileImageUrl = profileImageUrl
self.uid = uid
}
init?(document: DocumentSnapshot) {
let data = document.data()
let email = data!["email"] as? String ?? ""
let profileImageUrl = data!["profileImageUrl"] as? String ?? ""
let uid = data!["uid"] as? String ?? ""
id = document.documentID
self.email = email
self.profileImageUrl = profileImageUrl
self.uid = uid
}
enum CodingKeys: String, CodingKey {
case id
case email
case profileImageUrl
case uid
}
}
extension ChatUser: Comparable {
static func == (lhs: ChatUser, rhs: ChatUser) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: ChatUser, rhs: ChatUser) -> Bool {
return lhs.email < rhs.email
}
}
RecentMessage
struct RecentMessage: Codable, Identifiable, Hashable {
var id: String?
var text: String
var email: String
var fromId: String
var toId: String
var profileImageUrl: String
var timestamp: Date
init(text: String, email: String, fromId: String, toId: String, profileImageUrl: String, timestamp: Date, id: String?) {
self.id = id
self.text = text
self.email = email
self.fromId = email
self.toId = email
self.profileImageUrl = profileImageUrl
self.timestamp = timestamp
}
init?(document: DocumentSnapshot) {
let data = document.data()
let text = data!["text"] as? String ?? ""
let email = data!["email"] as? String ?? ""
let fromId = data!["fromId"] as? String ?? ""
let toId = data!["toId"] as? String ?? ""
let profileImageUrl = data!["profileImageUrl"] as? String ?? ""
let timestamp = data!["timestamp"] as? Date ?? Date()
id = document.documentID
self.text = text
self.email = email
self.fromId = fromId
self.toId = toId
self.profileImageUrl = profileImageUrl
self.timestamp = timestamp
}
enum CodingKeys: String, CodingKey {
case id
case text
case email
case fromId
case toId
case profileImageUrl
case timestamp
}
func getUsernameFromEmail() -> String {
return email.components(separatedBy: "#").first ?? email
}
func getElapsedTime() -> String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: timestamp, relativeTo: Date())
}
}
extension RecentMessage: Comparable {
static func == (lhs: RecentMessage, rhs: RecentMessage) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: RecentMessage, rhs: RecentMessage) -> Bool {
return lhs.timestamp < rhs.timestamp
}
}
ViewModels
One thing in advance, instead of using FirebaseManager.shared, I declared once in the ViewModel:
let db = Firestore.firestore()
I split the existing MessagesViewModel into MessagesViewModel and UserViewModel
I added a completion block to the fetchCurrentUser function.
It must be completed first (successfully) to run the fetchRecentMessage function. With that block we do 2 things:
make sure the fetchCurrentUser function is completed. I assume, even if the id is saved to the database, in your init function the fetchRecentMessage function will be called before you got the data for the currentUser. It is asynchronous and your code is not waiting for Firestore to be finished.
make sure that chatUser really is initialized with the values from Firestore.
UserViewModel
class UsersViewModel: ObservableObject {
let db = Firestore.firestore()
#Published var errorMessage = ""
#Published var chatUser: ChatUser?
func fetchCurrentUser(_ completion: #escaping (Bool, String) ->Void) {
guard let uid = Auth.auth().currentUser?.uid else {
self.errorMessage = "Could not find firebase uid"
completion(false, self.errorMessage)
return
}
let docRef = self.db.collection("chatUsers").document(uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
self.chatUser = ChatUser(document: document)
completion(true, "ChatUser set up from Firestore db")
} else {
self.errorMessage = "ChatUser document not found"
completion(false, self.errorMessage)
}
}
}
}
MessagesViewModel
class MessagesViewModel: ObservableObject {
let db = Firestore.firestore()
#Published var errorMessage = ""
#Published var recentMessages: [RecentMessage] = []
private var firestoreListener: ListenerRegistration?
func fetchData(_ completion: #escaping (Bool, String) ->Void) {
self.recentMessages.removeAll()
db.collection("recentMessages").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self.errorMessage = "No documents"
completion(true, self.errorMessage)
return
}
self.recentMessages = documents.map { queryDocumentSnapshot -> RecentMessage in
return RecentMessage(document: queryDocumentSnapshot)!
}
completion(true, "Data fetched")
}
}
}
Views
ContentView
The ContentView just opens the next view MusicBandFanView(), when the MusicBandFanView() appears it first calls the fetchCurrentUser() function and when the completion block of that function is completed, it calls the fetchRecentMessages() function
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var usersViewModel: UsersViewModel
#EnvironmentObject var messagesViewModel: MessagesViewModel
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
MusicBandFanView()
.onAppear {
usersViewModel.fetchCurrentUser({ (success, logMessage) -> Void in
if success {
print(logMessage)
messagesViewModel.fetchData({ (success, logMessage) -> Void in
print(logMessage)
})
} else {
print(logMessage)
}
})
}
}
}
MusicBandFanView
This is the first part of your view. I just change the messageView from a var within the MusicBandFanView to a distinct view.
Since I don't have your images you use, I used SF-Symbols instead.
struct MusicBandFanView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var usersViewModel: UsersViewModel
#EnvironmentObject var messagesViewModel: MessagesViewModel
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
VStack() {
HStack {
Button() {
} label: {
Image(systemName: "magnifyingglass")
.frame(width: 32, height: 32)
.padding(.leading, 11)
}
Spacer()
Image(systemName: "applelogo")
.frame(width: 32, height: 32)
.scaledToFill()
Spacer()
Button() {
} label: {
Image(systemName: "ellipsis")
.renderingMode(.template)
.frame(width: 32, height: 32)
.foregroundColor(Color(.init(red: 0.59, green: 0.62, blue: 0.67, alpha: 1)))
.padding(.trailing, 9)
.offset(y: -4)
}
}.padding(.init(top: 0, leading: 8, bottom: 0, trailing: 8))
NavigationView {
VStack {
Text("messages")
MessagesView()
}
}
}
}
}
MessagesView
That is what you put into a var.
I don't really got what you tried with the button, but I guess you can add that portion similar to the exiting code.
Since I didn't install SDWebimages for the test environment here, I just used an alternative SF-Symbol.
struct MessagesView: View {
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var usersViewModel: UsersViewModel
#EnvironmentObject var messagesViewModel: MessagesViewModel
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
ScrollView {
VStack {
ForEach(messagesViewModel.recentMessages, id: \.self){ recentMessage in
Button {
// I don't really get what you try to do here. The user is already logged in and the fetch for the chatUser (current user) has already been performed.
} label: {
HStack(spacing: 16) {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFill()
.frame(width: 64, height: 64)
.clipShape(Circle())
.overlay(Circle()
.stroke(Color.black, lineWidth: 1))
.shadow(radius: 5)
VStack(alignment: .leading, spacing: 8) {
Text(recentMessage.getUsernameFromEmail())
.font(.system(size: 16, weight: .bold))
.foregroundColor(Color(.label))
.multilineTextAlignment(.leading)
Text(recentMessage.text)
.font(.system(size: 14))
.foregroundColor(Color(.darkGray))
.multilineTextAlignment(.leading)
}
Spacer()
Text("\(recentMessage.getElapsedTime())")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Color(.label))
}
}
Divider()
.padding(.vertical, 8)
}
.padding(.horizontal)
}
.padding(.bottom, 50)
}
}
}
Screenshots
App Screenshot from simulator
Firestore Console Output ChatUser
Firestore Console Output RecentMessage

Can't add data fetched from Firebase to new Array

I'm am fetching data from firebase from 3 different collections. Once I have the data fetched I would like to append the data from the 3 functions to 1 new array so I have all the data stored one place. But once I append it comes out empty like the fetch functions didn't work. I've tested and debugged and the data is there but I can't seem to add the fetched data to a new Array.
Model
import Foundation
import SwiftUI
struct GameModel: Identifiable {
var id = UUID()
var title: String
var descriptionMenu: String
var imageNameMenu: String
}
Fetch Data Class
import Foundation
import SwiftUI
import Firebase
class SearchController: ObservableObject {
#Published var allGames = [GameModel]()
#Published var cardsMenu = [GameModel]()
#Published var diceMenu = [GameModel]()
#Published var miscellaneuosMenu = [GameModel]()
private var db = Firestore.firestore()
func fetchCardGamesData() {...}
func fetchDiceGamesData() {...}
func fetchMiscGamesData() {...}
func combineGames() {
for i in cardsMenu {
allGames.append(i)
}
for n in diceMenu {
allGames.append(n)
}
for x in miscellaneuosMenu {
allGames.append(x)
}
}
}
Fetch data Functions
func fetchCardGamesData() {
db.collection("cardsMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.cardsMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let allGameInfo = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return allGameInfo
}
}
}
func fetchDiceGamesData() {
db.collection("diceMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.diceMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let allGameInfo = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return allGameInfo
}
}
}
func fetchMiscGamesData() {
db.collection("miscellaneuosMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.miscellaneuosMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let miscellaneousGames = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return miscellaneousGames
}
}
}
View
import SwiftUI
import Foundation
struct SearchView: View {
#ObservedObject var allGames = SearchController()
var body: some View {
NavigationView{
ZStack(alignment: .top) {
GeometryReader{_ in
//Text("Home")
}
.background(Color("Color").edgesIgnoringSafeArea(.all))
SearchBar(data: self.$allGames.allGames)
.padding(.top)
}
.navigationBarTitle("Search")
.padding(.top, -20)
.onAppear(){
self.allGames.fetchCardGamesData()
self.allGames.fetchDiceGamesData()
self.allGames.fetchMiscGamesData()
self.allGames.combineGames()
}
}
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(PlainListStyle())
}
}
SearchBar
struct SearchBar: View {
#State var txt = ""
#Binding var data: [GameModel]
var body: some View {
VStack(spacing: 0){
HStack{
TextField("Search", text: self.$txt)
if self.txt != "" {
Button(action: {
self.txt = ""
}, label: {
Image(systemName: "xmark.circle.fill")
})
.foregroundColor(.gray)
}
}.padding()
if self.txt != ""{
if self.data.filter({$0.title.lowercased().contains(self.txt.lowercased())}).count == 0 {
Text("No Results Found")
.foregroundColor(Color.black.opacity(0.5))
.padding()
}
else {
List(self.data.filter{$0.title.lowercased().contains(self.txt.lowercased())}){
i in
NavigationLink(destination: i.view.navigationBarTitleDisplayMode(.inline).onAppear(){
// Clear searchfield when return
txt = ""
}) {
Text(i.title)
}
}
.frame(height: UIScreen.main.bounds.height / 3)
.padding(.trailing)
}
}
}
.background(Color.white)
.cornerRadius(10)
.padding()
}
}
Hope you guys can help me.
All of your fetchCardGamesData, fetchDiceGamesData, and fetchMiscGamesData functions have asynchronous requests in them. That means that when you call combineGames, none of them have completed, so you're just appending empty arrays.
In your situation, the easiest would probably be to make allGames a computed property. Then, whenever one of the other #Published properties updates after their fetch methods, the computed property will be re-computed and represented in your SearchBar:
class SearchController: ObservableObject {
#Published var cardsMenu = [GameModel]()
#Published var diceMenu = [GameModel]()
#Published var miscellaneuosMenu = [GameModel]()
var allGames: [GameModel] {
cardsMenu + diceMenu + miscellaneuosMenu
}
}
Note there's no longer a combineGames function, so you won't call that anymore.

Firebase is receiving requests but view won't show data

I have an app that works fine reading a JSON file but when I try to change the code to read the data from Firebase - after following a simpler tutorial - it does not show the data.
As a slight aside. I am only able to figure out how to import my JSON to the Realtime database and not Firestore. If it's not possible to import JSON to Firestore the I will need to rethink the approach as I aren't going to add all 7000 products by hand.
ProductDetail creates the style for a single product
struct ProductDetail: View {
#State private var username: String = ""
#State var product: Product_
var body: some View {
VStack{
Text(product.product_name)
.fontWeight(.bold)
.font(.title)
.padding()
HStack{
Text("EAN Code:")
Text(product.code).padding()
}
VStack(alignment: .leading){
HStack{
Text("Brand:")
.fontWeight(.bold)
Text(product.brands)
.padding()
}
Text("Packaging:")
.fontWeight(.bold)
VStack{
VStack(alignment: .leading){
HStack{
// If packaging1 has some value other than Unknown then show packaging1
if product.packaging1 != "Unknown" {
Text(product.packaging1)
.padding()
Text("Outcome")
}
// Otherwise include a button
else{
Button("Add Packaging"){}
.cornerRadius(8)
}
}
HStack{
if product.packaging1 != "Unknown" || product.packaging2 != "Unknown" {
Text(product.packaging2)
.padding()
Text("Outcome")
}
else if product.packaging1 != "Unknown" || product.packaging2 == "Unknown"{
Button("Add Packaging"){}
.cornerRadius(8)
}
}
HStack{
if product.packaging1 != "Unknown" || product.packaging2 != "Unknown" || product.packaging3 != "Unknown"{
Text(product.packaging3)
.padding()
Text("Outcome")
}
else {
Button("Add Packaging"){}
.cornerRadius(8)
}
}
}
}
}
}
}
}
ProductList provides a navigation view list of all the products.
struct ProductList: View {
#State private var searchText: String = ""
#ObservedObject private var viewModel = ProductsModel()
var body: some View {
NavigationView{
VStack{
//SearchBar(text: $searchText)
List(products, id: \.product_name) { product in
NavigationLink(destination: ProductDetail(product: product))
{ItemRow(product: product).navigationTitle("Products")
}
}
}
}
}
This is the structure that each product in the JSON (and soon to be Firebase) has.
struct Product_: Hashable, Codable{
var code: String
var brands: String
var product_name: String
var packaging1: String
var packaging2: String
var packaging3: String
}
This is basically the tutorial code but replaced with Products rather than books.
import Foundation
import FirebaseFirestore
class ProductsModel: ObservableObject{
#Published var product_fire = [Product_]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("product_fire").addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else{
print("No documents")
return
}
self.product_fire = documents.map{(queryDocumentSnapshot) -> Product_ in
let data = queryDocumentSnapshot.data()
let code = data["code"] as? String ?? ""
let brands = data["brands"] as? String ?? ""
let product_name = data["product_name"] as? String ?? ""
let packaging1 = data["packaging1"] as? String ?? ""
let packaging2 = data["packaging2"] as? String ?? ""
let packaging3 = data["packaging3"] as? String ?? ""
return Product_(code: code, brands:brands, product_name:product_name, packaging1:packaging1, packaging2:packaging2, packaging3:packaging3)
}
}
}
}
I have tried many things and the closest I have come is replacing the List() code with:
List(viewModel.product_fire, id: \.product_name) { product in
NavigationLink(destination: ProductDetail(product: product))
{ItemRow(product: product).navigationTitle("Products")
}.onAppear(){
self.viewModel.fetchData()
Edit to address XTwisteDX's comment:
let data = queryDocumentSnapshot.data()
let code = data["code"] as? String ?? "No Code"
let brands = data["brands"] as? String ?? "No Brand"
//let product_name = data["product_name"] as? String ?? ""
let packaging1 = data["packaging1"] as? String ?? "No Packaging1"
let packaging2 = data["packaging2"] as? String ?? "No Packaging2"
let packaging3 = data["packaging3"] as? String ?? "No Packaging3"
let product_name = "Test Name"
This still gives me a blank screen when I run the ProductList file. So I am guessing that it is a problem from passing the data from ProductData to ProductList
This just gives me a blank screen, but when I look on the Firebase console there is reads happening so at least I know the connection is correct and I think the problem is how I am displaying the data.
Many thanks, been stuck on this for days.
You're calling fetchData on the onAppear of every item in your List. You need to move it outside the List so that it only gets called once.
List(...) {
///
}.onAppear {
viewModel.fetchData()
}

SwiftUI: View does not update after image changed asynchronous

As mentioned in the headline, I try to load images to a custom object
I’ve got the custom object “User” that contains the property “imageLink” that stores the location within the Firebase Storage.
First I load the users frome the Firestore db and then I try to load the images for these users asynchronous from the Firebase Storage and show them on the View. As long as the image has not been loaded, a placeholder shall be shown.
I tried several implementations and I always can see in the debugger that I am able to download the images (I saw the actual image and I saw the size of some 100kb), but the loaded images don’t show on the view, I still see the placeholder, it seems that the view does not update after they loaded completely.
From my perspective, the most promising solution was:
FirebaseImage
import Combine
import FirebaseStorage
import UIKit
let placeholder = UIImage(systemName: "person")!
struct FirebaseImage : View {
init(id: String) {
self.imageLoader = Loader(id)
}
#ObservedObject private var imageLoader : Loader
var image: UIImage? {
imageLoader.data.flatMap(UIImage.init)
}
var body: some View {
Image(uiImage: image ?? placeholder)
}
}
Loader
import SwiftUI
import Combine
import FirebaseStorage
final class Loader : ObservableObject {
let didChange = PassthroughSubject<Data?, Never>()
var data: Data? = nil {
didSet { didChange.send(data) }
}
init(_ id: String){
// the path to the image
let url = "profilepics/\(id)"
print("load image with id: \(id)")
let storage = Storage.storage()
let ref = storage.reference().child(url)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
self.data = data
}
}
}
}
User
import Foundation
import Firebase
import CoreLocation
import SwiftUI
struct User: Codable, Identifiable, Hashable {
var id: String?
var name: String
var imageLink: String
var imagedata: Data = .init(count: 0)
init(name: String, imageLink: String, lang: Double) {
self.id = id
self.name = name
self.imageLink = imageLink
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let name = data["name"] as? String else {
return nil
}
guard let imageLink = data["imageLink"] as? String else {
return nil
}
id = document.documentID
self.name = name
self.imageLink = imageLink
}
}
extension User {
var image: Image {
Image(uiImage: UIImage())
}
}
extension User: DatabaseRepresentation {
var representation: [String : Any] {
var rep = ["name": name, "imageLink": imageLink] as [String : Any]
if let id = id {
rep["id"] = id
}
return rep
}
}
extension User: Comparable {
static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: User, rhs: User) -> Bool {
return lhs.name < rhs.name
}
}
UserViewModel
import Foundation
import FirebaseFirestore
import Firebase
class UsersViewModel: ObservableObject {
let db = Firestore.firestore()
let storage = Storage.storage()
#Published var users = [User]()
#Published var showNewUserName: Bool = UserDefaults.standard.bool(forKey: "showNewUserName"){
didSet {
UserDefaults.standard.set(self.showNewUserName, forKey: "showNewUserName")
NotificationCenter.default.post(name: NSNotification.Name("showNewUserNameChange"), object: nil)
}
}
#Published var showLogin: Bool = UserDefaults.standard.bool(forKey: "showLogin"){
didSet {
UserDefaults.standard.set(self.showLogin, forKey: "showLogin")
NotificationCenter.default.post(name: NSNotification.Name("showLoginChange"), object: nil)
}
}
#Published var isLoggedIn: Bool = UserDefaults.standard.bool(forKey: "isLoggedIn"){
didSet {
UserDefaults.standard.set(self.isLoggedIn, forKey: "isLoggedIn")
NotificationCenter.default.post(name: NSNotification.Name("isLoggedInChange"), object: nil)
}
}
func addNewUserFromData(_ name: String, _ imageLing: String, _ id: String) {
do {
let uid = Auth.auth().currentUser?.uid
let newUser = User(name: name, imageLink: imageLing, lang: 0, long: 0, id: uid)
try db.collection("users").document(newUser.id!).setData(newUser.representation) { _ in
self.showNewUserName = false
self.showLogin = false
self.isLoggedIn = true
}
} catch let error {
print("Error writing city to Firestore: \(error)")
}
}
func fetchData() {
db.collection("users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.map { queryDocumentSnapshot -> User in
let data = queryDocumentSnapshot.data()
let id = data["id"] as? String ?? ""
let name = data["name"] as? String ?? ""
let imageLink = data["imageLink"] as? String ?? ""
let location = data["location"] as? GeoPoint
let lang = location?.latitude ?? 0
let long = location?.longitude ?? 0
Return User(name: name, imageLink: imageLink, lang: lang, long: long, id: id)
}
}
}
}
UsersCollectionView
import SwiftUI
struct UsersCollectionView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var usersViewModel: UsersViewModel
let itemWidth: CGFloat = (screenWidth-30)/4.2
let itemHeight: CGFloat = (screenWidth-30)/4.2
var fixedLayout: [GridItem] {
[
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2)),
.init(.fixed((screenWidth-30)/4.2))
]
}
func debugUserValues() {
for user in usersViewModel.users {
print("ID: \(user.id), Name: \(user.name), ImageLink: \(user.imageLink)")
}
}
var body: some View {
VStack() {
ScrollView(showsIndicators: false) {
LazyVGrid(columns: fixedLayout, spacing: 15) {
ForEach(usersViewModel.users, id: \.self) { user in
VStack() {
FirebaseImage(id: user.imageLink)
HStack(alignment: .center) {
Text(user.name)
.font(.system(size: 16))
.fontWeight(.bold)
.foregroundColor(Color.black)
.lineLimit(1)
}
}
}
}
.padding(.top, 20)
Rectangle()
.fill(Color .clear)
.frame(height: 100)
}
}
.navigationTitle("Find Others")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
.foregroundColor(.black)
.padding()
.offset(x: -15)
}
})
}
}
You're using an old syntax from BindableObject by using didChange -- that system changed before SwiftUI 1.0 was out of beta.
A much easier approach would be to use #Published, which your view will listen to automatically:
final class Loader : ObservableObject {
#Published var data : Data?
init(_ id: String){
// the path to the image
let url = "profilepics/\(id)"
print("load image with id: \(id)")
let storage = Storage.storage()
let ref = storage.reference().child(url)
ref.getData(maxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
print("\(error)")
}
DispatchQueue.main.async {
self.data = data
}
}
}
}