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

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

Related

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

List not re-loading from firestore in base view on dismiss of .sheet in SwiftUI

I have another question.
I have a demo app where I add a ToDo in a Firestore database. From the base View I open a .sheet with a TextEditor where I enter data and save it into Firestore database. But on dismiss the List of ToDos in the base View is gone and is not refreshed until I go to another tab in the app and return back.
I have a ViewModel where I use a Firebase snapshot listener.
Code of the base View:
import Firebase
import Foundation
import SwiftUI
import FirebaseStorage
struct HomeMenuView: View {
#ObservedObject var toDosViewModel = ToDosViewModel()
#Binding var showAddToDoView: Bool
#State private var showModifyToDoView = false
#State private var note = ""
#State private var selectedToDoId = ""
func removeRow(at offset:IndexSet) {
for index in offset {
toDosViewModel.deleteNote(noteToDelete: toDosViewModel.todos[index].id!)
}
}
var body: some View {
ZStack{
VStack (alignment: .center){
List() {
ForEach(toDosViewModel.todos) { todo in
VStack(alignment: .leading, spacing: 10) {
Text(todo.notes)
.font(.subheadline)
.foregroundColor(Color.tabBarColor)
.lineLimit(2)
.onTapGesture {
showAddToDoView = true
selectedToDoId = todo.id!
note = todo.notes
}
}
.listRowSeparatorTint(Color.tabBarColor)
}
.onDelete(perform: removeRow)
}
.listStyle(InsetGroupedListStyle())
.onAppear() {
toDosViewModel.subscribe()
}
}
}
.sheet(isPresented: $showAddToDoView) {
VStack() {
HStack () {
Button("Save") {
guard !note.isEmpty else
{ showAddToDoView = false; return }
toDosViewModel.addNote(notes: note)
note = ""
showAddToDoView = false
}
.offset(x: 20)
Spacer()
Button("Back") {
note = ""
showAddToDoView = false
}
.offset(x: -20)
}
.frame(height: 50, alignment: .center)
TextEditor(
text: $note
)
}
}
}
}
The ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
import UIKit
class ToDosViewModel: ObservableObject {
#Published var todos = [ToDo]()
#Published var errorMessage: String?
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("todos")
.order(by: "timestamp", descending: true)
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self?.todos = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ToDo.self) }
switch result {
case .success(let todo):
if let todo = todo {
self?.errorMessage = nil
return todo
}
else {
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}
func addNote(notes: String) {
db.collection("todos").document().setData(["notes" : notes, "timestamp" : FieldValue.serverTimestamp()])
}
func modifyNote(noteToModify: String, notes: String) {
db.collection("todos").document(noteToModify).setData(["notes" : notes, "timestamp" : FieldValue.serverTimestamp()])
}
func deleteNote(noteToDelete: String) {
db.collection("todos").document(noteToDelete).delete()
}
}
Any idea what the issue could be?
Thanks a lot for your support.
Change
#ObservedObject var toDosViewModel = ToDosViewModel()
To
#StateObject var toDosViewModel = ToDosViewModel()

Why I am not able to register user info to firebase in SwiftUI App?

I have register screen and I am not able to register user info to firebase,
I put user info on simulator, but it just hold on on screen, and no any change on firebase, I guess maybe there is missed points on
CreateUser
function, any idea?
import SwiftUI
import Firebase
struct Register: View {
#State var name = ""
#State var about = ""
#Binding var show : Bool
var body: some View {
VStack(alignment: .center, spacing: 3){
TextField("Name",text:self.$name)
.padding()
TextField(" about", text: self.$about)
.padding()
if self.loading{
HStack{
Spacer()
Indicator()
Spacer()
}
}
else{
Button {
if self.name != "" && self.about != "" {
self.loading.toggle()
CreateUser(name: self.name, about: self.about) { (status) in
if status{
self.show.toggle()
}
}
}
else{
self.alert.toggle()
}
} label: {
Text("Next")
.padding()
}
}
}
user:
import Foundation
import Firebase
func CreateUser(name: String,about : String, completion : #escaping (Bool)-> Void){
let db = Firestore.firestore()
let storage = Storage.storage().reference()
let uid = Auth.auth().currentUser?.uid
db.collection("users").document(uid!).setData(["name":name,"about":about, "uid":uid!]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
completion(true)
UserDefaults.standard.set(true, forKey: "status")
UserDefaults.standard.set(name, forKey: "UserName")
NotificationCenter.default.post(name: NSNotification.Name("statusChange"), object: nil)
}
}

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