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

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

Related

Why is it that every time I call fetchfollowingposts, the post user changes?

Any time the function "fetchfollowingposts" is called, the user's information in the feed cell changes. I've saved the post to firebase with the user's uid and i'm trying to fetch their profilephoto, fullname, etc. from the uid tied to the post. Any time I refresh the feedview, the user's information changes but the post itself never does (timestamp, post caption, post image, likes).
Since I don't fully understand the problem, I wasn't sure what files are needed so just let me know if i missed one. Thank you in advance for any help!
FeedCellView
import SwiftUI
import Kingfisher
struct FeedCell: View {
#ObservedObject var viewModel: FeedCellViewModel
#State private var isShowingBottomSheet = false
var didLike: Bool { return viewModel.post.didLike ?? false }
#Environment(\.presentationMode) var mode
init(viewModel: FeedCellViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack (alignment: .leading, spacing: 16) {
NavigationLink {
if let user = viewModel.post.user {
LazyView(ProfileView(user: user))
}
} label: {
HStack (alignment: .top) {
KFImage(URL(string: viewModel.post.user?.profileImageUrl ?? "https://firebasestorage.googleapis.com/v0/b/pageturner-951b4.appspot.com/o/profile_image%2FNoProfilePhoto.png?alt=media&token=1055648d-4d6e-4d51-b003-948a47b6bb76"))
.resizable()
.scaledToFill()
.frame(width: 48, height: 48)
.cornerRadius(10)
VStack (alignment: .leading, spacing: 4) {
Text(viewModel.post.user?.fullname ?? "")
.font(.system(size: 16))
.foregroundColor(Color(.label))
Text(viewModel.timestampString)
.font(.system(size: 14))
.foregroundColor(.gray)
}
Spacer()
if viewModel.post.isCurrentUser {
Button {
isShowingBottomSheet.toggle()
} label: {
Image(systemName: "ellipsis")
}.foregroundColor(Color(.gray))
.confirmationDialog("What do you want to do?",
isPresented: $isShowingBottomSheet) {
Button("Delete post", role: .destructive) {
viewModel.deletePost()
}
} message: {
Text("You cannot undo this action")
}
}
}
}
Text(viewModel.post.caption)
.font(.system(size: 16))
if let image = viewModel.post.imageUrl {
KFImage(URL(string: image))
.resizable()
.scaledToFill()
.frame(maxHeight: 250)
.cornerRadius(10)
}
HStack {
HStack (spacing: 24) {
Button {
didLike ? viewModel.unlike() : viewModel.like()
} label: {
Image(didLike ? "heart.fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundColor(didLike ? Color.accentColor : .black)
.frame(width: 24, height: 24)
Text("\(viewModel.post.likes)")
}
NavigationLink {
CommentView(post: viewModel.post)
} label: {
Image("comment")
.renderingMode(.template)
.resizable()
.frame(width: 24, height: 24)
Text("\(viewModel.post.stats?.CommentCount ?? 0)")
}
}
Spacer()
}
.foregroundColor(Color(.label))
}
}
}
FeedService
import SwiftUI
import FirebaseCore
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
struct FeedService {
func uploadPost(caption: String, image: UIImage?, completion: #escaping(Bool) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
ImageUploader.uploadImage(image: image, type: .post) { imageUrl in
let data = ["uid": uid,
"caption": caption,
"likes": 0,
"imageUrl": imageUrl,
"timestamp": Timestamp(date: Date())] as [String: Any]
COLLECTION_POSTS.document()
.setData(data) { error in
if let error = error {
print("DEBUG: Failed to upload post with error: \(error.localizedDescription)")
completion(false)
return
}
}
completion(true)
}
}
func fetchFollowingPosts(forUid uid: String, completion: #escaping([Post]) -> Void) {
var posts = [Post]()
COLLECTION_FOLLOWING.document(uid).collection("user-following")
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
documents.forEach { doc in
let userId = doc.documentID
COLLECTION_POSTS.whereField("uid", isEqualTo: userId)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
let post = documents.compactMap({ try? $0.data(as: Post.self) })
posts.append(contentsOf: post)
completion(posts.sorted(by: { $0.timestamp.dateValue() > $1.timestamp.dateValue()
}))
}
}
}
}
func uploadStory(caption: String?, image: UIImage, rating: Int?, completion: #escaping(Bool) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
ImageUploader.uploadImage(image: image, type: .story) { imageUrl in
let data = ["uid": uid,
"caption": caption ?? "",
"imageUrl": imageUrl,
"rating": rating ?? "",
"isSeen": false,
"timestamp": Timestamp(date: Date())] as [String: Any]
COLLECTION_STORIES.document()
.setData(data) { error in
if let error = error {
print("DEBUG: Failed to upload story with error: \(error.localizedDescription)")
completion(false)
return
}
}
completion(true)
}
}
func fetchFollowingStories(forUid uid: String, completion: #escaping([Story]) -> Void) {
var stories = [Story]()
COLLECTION_FOLLOWING.document(uid).collection("user-following")
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
documents.forEach { doc in
let userId = doc.documentID
COLLECTION_STORIES.whereField("uid", isEqualTo: userId)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
let story = documents.compactMap({ try? $0.data(as: Story.self) })
stories.append(contentsOf: story)
completion(stories.sorted(by: { $0.timestamp.dateValue() > $1.timestamp.dateValue()
}))
}
}
}
}
}
FeedViewModel
import SwiftUI
class FeedViewModel: ObservableObject {
#Published var followingPosts = [Post]()
#Published var followingStories = [Story]()
let service = FeedService()
let userService = UserService()
init() {
fetchFollowingPosts()
fetchFollowingStories()
}
func fetchFollowingPosts() {
guard let userid = AuthViewModel.shared.userSession?.uid else { return }
service.fetchFollowingPosts(forUid: userid) { posts in
self.followingPosts = posts
for i in 0 ..< posts.count {
let uid = posts[i].uid
self.userService.fetchUser(withUid: uid) { user in
self.followingPosts[i].user = user
}
}
}
}
func fetchFollowingStories() {
guard let userid = AuthViewModel.shared.userSession?.uid else { return }
service.fetchFollowingStories(forUid: userid) { stories in
self.followingStories = stories
for i in 0 ..< stories.count {
let uid = stories[i].uid
self.userService.fetchUser(withUid: uid) { user in
self.followingStories[i].user = user
}
}
}
}
}

Firestore not returning data to #Published variable

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

Closing List view in SwiftUI

I have an app which reads a qr code, and then hits an api after using a key from the qr code, my problem now is, after it finds the data, I cannot figure out how to add a button which removes the layered list view to return to the root.. I have tried several solutions found here and on google and read through a lot of documentation, but none seemed to work..
import SwiftUI
import CodeScanner
extension URL {
var components: URLComponents? {
return URLComponents(url: self, resolvingAgainstBaseURL: false)
}
}
extension Array where Iterator.Element == URLQueryItem {
subscript(_ key: String) -> String? {
return first(where: { $0.name == key })?.value
}
}
struct Card: Decodable,Identifiable {
let id = UUID()
let sport: String
let year: String
let brand: String
let cardNumber: String
let playerName: String
let extra: String
let gradeName: String
let grade: String
let serial: String
let authDate: String
}
class apiCall {
func getUsers(apihit: String, completion:#escaping ([Card]) -> ()) {
guard let apihit = URL(string: apihit) else { return }
URLSession.shared.dataTask(with: apihit) { (data, _, _) in
let users = try! JSONDecoder().decode([Card].self, from: data!)
print(users)
DispatchQueue.main.async {
completion(users)
}
}
.resume()
}
}
struct ContentView: View {
#State var isPresentingScanner = false
#State var scannedCode: String = ""
#State var users: [Card] = []
var scannerSheet : some View {
CodeScannerView(
codeTypes: [.qr],
completion: { result in
if case let .success(code) = result {
self.scannedCode = code.string
self.isPresentingScanner = false
}
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
func getQueryStringParameter(url: String, param: String) -> String? {
guard let url = URLComponents(string: url) else { return nil }
return url.queryItems?.first(where: { $0.name == param })?.value
}
func getDateFromTimeStamp(timeStamp : Double) -> String {
let date = NSDate(timeIntervalSince1970: timeStamp / 1000)
let dayTimePeriodFormatter = DateFormatter()
dayTimePeriodFormatter.dateFormat = "dd MMM YY, hh:mm a"
let dateString = dayTimePeriodFormatter.string(from: date as Date)
return dateString
}
var body: some View {
VStack(spacing: 10) {
Image("logo-white")
.offset(y: -200)
if let urlComponents = URL(string: scannedCode)?.components,
let cert = urlComponents.queryItems?["certificateNumber"] {
//Text(cert)
let apihit = URL(string: "https://app.example.com/api.php?apikey=xxxx&cert=\(cert)")!
NavigationView {
List(users) { user in
Text("Set: " + user.year + " " + user.brand)
.font(.headline)
if !user.extra.isEmpty {
Text("Desc: " + user.extra)
.font(.headline)
}
Text("Player: " + user.playerName)
.font(.headline)
Text("Year: " + user.year)
.font(.headline)
Text("Sport: " + user.sport)
.font(.headline)
Text("Grade Name: " + user.gradeName)
.font(.headline)
Text("Grade: " + user.grade)
.font(.headline)
Text("Card Serial: " + user.serial)
.font(.headline)
Text("Authenticated: " + user.authDate)
.font(.headline)
}
.onAppear {
apiCall().getUsers(apihit: apihit.absoluteString) { (users) in
self.users = users
}
}
.navigationTitle("Certificate Verification")
}
}
Button("Scan QR Code") {
self.isPresentingScanner = true
}
.padding()
.background(Color(red: 0, green: 0, blue: 0.5))
.foregroundColor(.white)
.clipShape(Rectangle())
.cornerRadius(20)
.sheet(isPresented: $isPresentingScanner) {
self.scannerSheet
Button("Close") {
self.isPresentingScanner = false
}
.padding()
.background(Color(red: 0, green: 0, blue: 0.5))
.foregroundColor(.white)
.clipShape(Rectangle())
.cornerRadius(20)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
an image of the view, which also doesn't stretch across the whole screen..
I resolved this by creating a new sheet for the list view, which now allows me to close it correctly.

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
}
}
}
}

SwiftUI can't get image from download url

I have the following code to load an image from a download url and display it as an UIImage.
I expected it to work, but somehow, solely the placeholder image 'ccc' is being displayed, and not the actual image from the download url. How so?
My urls are being fetched from a database and kind of look like this:
https://firebasestorage.googleapis.com/v0/b/.../o/P...alt=media&token=...-579f...da
struct ShelterView: View {
var title: String
var background: String
var available: Bool
var distance: Double
var gender: String
#ObservedObject private var imageLoader: Loader
init(title: String, background: String, available: Bool, distance: Double, gender: String) {
self.title = title
self.background = background
self.available = available
self.distance = distance
self.gender = gender
self.imageLoader = Loader(background)
}
var image: UIImage? {
imageLoader.data.flatMap(UIImage.init)
}
var body: some View {
VStack {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 0) {
Text(title)
.font(Font.custom("Helvetica Now Display Bold", size: 30))
.foregroundColor(.white)
.padding(15)
.lineLimit(2)
HStack(spacing: 25) {
IconInfo(image: "bed.double.fill", text: String(available), color: .white)
if gender != "" {
IconInfo(image: "person.fill", text: gender, color: .white)
}
}
.padding(.leading, 15)
}
Spacer()
IconInfo(image: "mappin.circle.fill", text: String(distance) + " miles away", color: .white)
.padding(15)
}
Spacer()
}
.background(
Image(uiImage: image ?? UIImage(named: "ccc")!) <-- HERE
.brightness(-0.11)
.frame(width: 255, height: 360)
)
.frame(width: 255, height: 360)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
}
final class Loader: ObservableObject {
var task: URLSessionDataTask!
#Published var data: Data? = nil
init(_ urlString: String) {
print(urlString)
let url = URL(string: urlString)
task = URLSession.shared.dataTask(with: url!, completionHandler: { data, _, _ in
DispatchQueue.main.async {
self.data = data
}
})
task.resume()
}
deinit {
task.cancel()
}
}
Your image is a plain old var which happens to be nil when the View is built. SwiftUI only rebuilds itself in response to changes in #ObservedObject, #State, or #Binding, so move your image to an #Published property on your imageLoader and it will work. Here is my caching image View:
import SwiftUI
import Combine
import UIKit
class ImageCache {
enum Error: Swift.Error {
case dataConversionFailed
case sessionError(Swift.Error)
}
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private init() { }
static func image(for url: URL?) -> AnyPublisher<UIImage?, ImageCache.Error> {
guard let url = url else {
return Empty().eraseToAnyPublisher()
}
guard let image = shared.cache.object(forKey: url as NSURL) else {
return URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { (tuple) -> UIImage in
let (data, _) = tuple
guard let image = UIImage(data: data) else {
throw Error.dataConversionFailed
}
shared.cache.setObject(image, forKey: url as NSURL)
return image
}
.mapError({ error in Error.sessionError(error) })
.eraseToAnyPublisher()
}
return Just(image)
.mapError({ _ in fatalError() })
.eraseToAnyPublisher()
}
}
class ImageModel: ObservableObject {
#Published var image: UIImage? = nil
var cacheSubscription: AnyCancellable?
init(url: URL?) {
cacheSubscription = ImageCache
.image(for: url)
.replaceError(with: nil)
.receive(on: RunLoop.main, options: .none)
.assign(to: \.image, on: self)
}
}
struct RemoteImage : View {
#ObservedObject var imageModel: ImageModel
private let contentMode: ContentMode
init(url: URL?, contentMode: ContentMode = .fit) {
imageModel = ImageModel(url: url)
self.contentMode = contentMode
}
var body: some View {
imageModel
.image
.map { Image(uiImage:$0).resizable().aspectRatio(contentMode: contentMode) }
?? Image(systemName: "questionmark").resizable().aspectRatio(contentMode: contentMode)
}
}