Display query values within view's body - google-cloud-firestore

What I want to do: I want to display the user's name on the Profile view after a user logs into the app. Currently I have implemented a login using Firebase Auth. When users create an account, it creates a record in the "Users" collection in Firestore that records First and Last Name and email address.
What I've Tried: On the Profile view, I currently have a function "getUser" that checks if the user is logged in, and then matches the user's Firebase Auth email address and matches it to the record in Firestore. This is working because I've checked what info the query is returning by what's logged in the console. However, I'm at a lost how to get the "fName" information and displaying it within the view's body.
Here's screenshots of the database structure and my current code.
import SwiftUI
import Firebase
import Combine
import FirebaseFirestore
struct ProfileView: View {
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func getUser() {
session.listen()
let query = db.collection("users").whereField("email", isEqualTo: session.session!.email!)
query.getDocuments { (QuerySnapshot, err) in
if let docs = QuerySnapshot?.documents {
for docSnapshot in docs {
print (docSnapshot.data())
}
}
}
}
var body: some View {
Group {
if (session.session != nil) {
NavigationView {
VStack {
Text("Welcome back \(session.session!.email ?? "user")")
Spacer()
Button(action: session.signOut) {
Text("Sign Out")
}.padding(.bottom, 60)
}
} // end NavigationView
} else {
AuthView()
}
}.onAppear(perform: getUser)
}
}

I guess you should save the values returned from Firestore with UserDefaults
import UIKit
//your get user data logic here
let fName:String = <the first name retrieved from firestore>
// Get the standard UserDefaults as "defaults"
let defaults = UserDefaults.standard
// Save the String to the standard UserDefaults under the key, "first_name"
defaults.set(fName, forKey: "first_name")
And in your profile page you should retrieve this value
// you should have defaults initialized here
fnameToDisplay = defaults.string(forKey: "first_name")

Hello I prefer to make a ViewModel - NetworkManager class that comfort ObservableObject and inside of it you can get and fetch data and when they finish loading they will update the view, Let me show you an example how it works:
as you can see I created a NetworkManager extends ObservableObject (cause I want to get notified when the user finish loading) inside of it you can see that var user is annotated with #Published that make the user observed and when a new data set will notify the View to update. also you can see that I load the data on init so the fist time this class initialized it will call the fetchData() func
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
ContentView is where I'm gonna place the user data or info I declared #ObservedObject var networkManager cause it extend ObservableObject and I place the views and pass User
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
class User: Decodable, ObservableObject {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: I didn't make objects unwrapped correctly please consider that so
you not to have any nil exception

Related

SwiftUI Read Data from Firebase to View and Function

I'm still a beginner and have a question, I'm trying to read data from the database with Firebase how can I pass this to a text? I want to pass "testUser" and display it.
anyone have an idea :-)
import SwiftUI
import FirebaseDatabase
import Firebase
struct uebergeben: View {
var body: some View {
VStack {
Text(datenLesen)
}
}
func datenLesen() {
let user = Auth.auth().currentUser!.uid
Database.database().reference().child("benutzer").child(user).child("nuterinfo").child("nutzername").observeSingleEvent(of: .value, with: { (snapshot) in
if let testUser = snapshot.value as? String {
print(testUser)
}
})
}
}
struct uebergeben_Previews: PreviewProvider {
static var previews: some View {
uebergeben(test: "Daten Abrufen")
}
}
You can use the #State properties inside of your view. For more details about #State you can read in the documentation. In short, the #State property is a property that is responsible for the state of the view. When the value of this property changes it means that the state of the views also changes and our view redraws itself.
struct uebergeben: View {
#State
private var user: String = "" // Declare the view's state property
var body: some View {
VStack {
Text(user)
}
.onAppear {
datenLesen() // Call the method for the fetching data
}
}
func datenLesen() {
let user = Auth.auth().currentUser!.uid
Database.database().reference().child("benutzer").child(user).child("nuterinfo").child("nutzername").observeSingleEvent(of: .value, with: { (snapshot) in
if let testUser = snapshot.value as? String {
self.user = user // Change the value of the state property of the view
}
})
}
}
Feel free if want to ask something additional about this or you have some questions for code

How Can You Reinstantiate EnvironmentObjects is SwiftUI?

I am working on a SwiftUI project and using Firebase. Part of the app allows users to add business addresses to their account. In Firebase I add a BusinessAddress as a sub collection to the user.
User -> BusinessAddresses
In iOS I am using snapshotlistners and Combine to get the data from Firestore. My issue happens when one user logs out and a new user logs in. The business addresses from the first user continue to show in the view that is designated to show business addresses.
I've confirmed the Firebase code is working properly. What seems to be happening is that the creation of BusinessAddressViewModel maintains the original instance of BusinessAddressRepository from when the first user logs in.
My question is how do I "reset" the instance of businessAddressRepository in my BusinessAddressViewModel when a new user logs in?
BusinessAddressRepository
This file gets the data from Firebase. It gets the currently user that is logged in and pulls the business addresses for this user. For each log in, I can see that the proper user and addresses are being updated here.
class BusinessAddressRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var businessAddresses = [BusinessAddress]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
guard let currentUserId = Auth.auth().currentUser else {
return
}
if snapshotListener == nil {
self.snapshotListener = db.collection(FirestoreCollection.users).document(currentUserId.uid).collection(FirestoreCollection.businessAddresses).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Business Addresses.")
return
}
self.businessAddresses = documents.compactMap { businessAddress in
do {
print("This is a business address *********** \(businessAddress.documentID)")
return try businessAddress.data(as: BusinessAddress.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
}
BusinessAddressViewModel
This file uses Combine to monitor the #published property businessAddresses from bankAccountRepository. This seems to be where the problem is. For some reason, this is holding on to the instance that is created when the app starts up.
class BusinessAddressViewModel: ObservableObject {
var businessAddressRepository: BusinessAddressRepository
#Published var businessAddressRowViewModels = [BusinessAddressRowViewModel]()
private var cancellables = Set<AnyCancellable>()
init(businessAddressRepository: BusinessAddressRepository) {
self.businessAddressRepository = businessAddressRepository
self.startCombine()
}
func startCombine() {
businessAddressRepository
.$businessAddresses
.receive(on: RunLoop.main)
.map { businessAddress in
businessAddress
.map { businessAddress in
BusinessAddressRowViewModel(businessAddress: businessAddress)
}
}
.assign(to: \.businessAddressRowViewModels, on: self)
.store(in: &cancellables)
}
}
BusinessAddressViewModel is set up and an #EnvironmentObject in main.
Main
#StateObject private var businessAddressViewModel = BusinessAddressViewModel(businessAddressRepository: BusinessAddressRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(businessAddressViewModel)
}
I was able to confirm it's hanging on to the instance in the view BusinessAddressView. Here I use a List that looks at each BusinessAddress in BusinessAddressViewModel. I use print statements in the ForEach and an onAppear that show the BusinessAddresses from the first user.
BusinessAddressView
struct BusinessAddressView: View {
let db = Firestore.firestore()
#EnvironmentObject var businessAddressViewModel: BusinessAddressViewModel
var body: some View {
List {
ForEach(self.businessAddressViewModel.businessAddressRowViewModels, id: \.id) { businessAddressRowViewModel in
let _ = print("View Business Addresses \(businessAddressRowViewModel.businessAddress.id)")
NavigationLink(destination: BusinessAddressDetailView(businessAddressDetailViewModel: BusinessAddressDetailViewModel(businessAddress: businessAddressRowViewModel.businessAddress, businessAddressRepository: businessAddressViewModel.businessAddressRepository))
) {
BusinessAddressRowView(businessAddressRowViewModel: businessAddressRowViewModel)
}
} // ForEach
} // List
.onAppear(perform: {
for businessAddress in self.businessAddressViewModel.businessAddressRepository.businessAddresses {
let _ = print("On Appear Business Addresses \(businessAddress.id)")
}
}) // CurrentUserUid onAppear
} // View
}
So, how do I reset the instance of BusinessAddressViewModel and get it to look at the current BusinessAddressRepository?

SwiftUI Firebase Authentication dismiss view after successfully login

I'm a beginner iOS developer and I have a problem with my first application. I'm using Firebase as a backend for my app and I have already sign in and sing up methods implemented. My problem is with dismissing LoginView after Auth.auth().signIn method finishing. I've managed to do this when I'm using NavigationLink by setting ObservableObject in isActive:
NavigationLink(destination: DashboardView(), isActive: $isUserLogin) { EmptyView() }
It's working as expected: when app ending login process screen is going to next view - Dashboard.
But I don't want to use NavigationLink and creating additional step, I want just go back to Dashboard using:
self.presentationMode.wrappedValue.dismiss()
In this case I don't know how to force app to wait till method loginUser() ends. This is how my code looks now:
if loginVM.loginUser() {
appSession.isUserLogin = true
self.presentationMode.wrappedValue.dismiss()
}
I've tried to use closures but it doesn't work or I'm doing something wrong.
Many thanks!
You want to use a AuthStateDidChangeListenerHandle and #EnvrionmentObject, like so:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userSession: UserModel? { didSet { self.willChange.send(self) }}
var willChange = PassthroughSubject<SessionStore, Never>()
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({ [weak self] (auth, user) in
if let user = user {
let firestoreUserID = API.FIRESTORE_DOCUMENT_USER_ID(userID: user.uid)
firestoreUserID.getDocument { (document, error) in
if let dict = document?.data() {
//Decoding the user, you can do this however you see fit
guard let decoderUser = try? UserModel.init(fromDictionary: dict) else {return}
self!.userSession = decoderUser
}
}
self!.isLoggedIn = true
} else {
self!.isLoggedIn = false
self!.userSession = nil
}
})
}
func logOut() {
do {
try Auth.auth().signOut()
print("Logged out")
} catch let error {
debugPrint(error.localizedDescription)
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
print("deinit - seession store")
}
}
Then simply do something along these lines:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
ZStack {
Color(SYSTEM_BACKGROUND_COLOUR)
.edgesIgnoringSafeArea(.all)
Group {
if session.isLoggedIn {
DashboardView()
} else if !session.isLoggedIn {
SignInView()
}
}
}.onAppear(perform: listen)
}
}
Then in your app file, you'd have this:
InitialView()
.environmentObject(SessionStore())
By using an #EnvironmentObject you can now access the user from any view, furthermore, this also allows to track the Auth status of the user meaning if they are logged in, then the application will remember.

Firestore method in view model not invoking in SwiftUI onAppear method

I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:
// ViewModel.swift
import Foundation
import Firebase
import FirebaseFirestore
class UserViewModel: ObservableObject {
let current_user_id = Auth.auth().currentUser!.uid
private var db = Firestore.firestore()
#Published var xp:Int?
func fetchData(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(current_user_id)
docRef.getDocument { snapshot, error in
print(error ?? "No error.")
self.xp = 0
guard let snapshot = snapshot else {
completion()
return
}
self.xp = (snapshot.data()!["xp"] as! Int)
completion()
}
}
}
// View.swift
import SwiftUI
import CoreData
import Firebase
{
#ObservedObject private var users = UserViewModel()
var body: some View {
VStack {
HStack {
// ...
Text("xp: \(users.xp ?? 0)")
// Text("xp: 1500")
.fontWeight(.bold)
.padding(.horizontal)
.foregroundColor(Color.white)
.background(Color("Black"))
.clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
.padding(.trailing)
}
.padding(.top)
.onAppear() {
self.users.fetchData()
}
// ...
}
}
My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?
I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.
That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.
class UserViewModel: ObservableObject {
#Published var xp: Int?
init() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let docRef = Firestore.firestore().collection("users").document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.xp = xp
} else if let error = error {
print(error)
}
}
}
}
struct ContentView: View {
#ObservedObject var users = UserViewModel()
var body: some View {
VStack {
HStack {
Text("xp: \(users.xp ?? 0)")
}
}
}
}
SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.

SwiftUI - Show the data fetched from Firebase in view?

Firebase I am trying to show data taken from Firestore in my SwiftUI view but I have a problem. I have no problem with pulling data from Firebase. But I cannot show the data as I want. I work with MVVM architecture.
My model is like this:
struct UserProfileModel: Identifiable {
#DocumentID var id : String?
var username : String
var uidFromFirebase : String
var firstName : String
var lastName : String
var email : String
}
ViewModel:
class UserProfileViewModel: ObservableObject {
#Published var user : [UserProfileModel] = []
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}
}
}
}
View:
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View { // -> Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
VStack{
Text(viewModel.user.username) // -> I want to do this but XCode is giving an error.
// This works but I don't want to do it this way.
List(viewModel.user) { user in
VStack{
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
}
In the videos and articles titled "SwiftUI fetch data from Firebase" that I watched and read, I have always narrated on List and ForEach. But I want to use the data wherever. I shared all my code with you. I want to learn how I can do this.
Looks to me like you really just want to have one user that you're pulling the data for, but you've set up your UserProfileViewModel with an array of users ([UserProfileModel]). There are a number of ways that you could take care of this. Here's one (check the code for inline comments about what is going on):
class UserProfileViewModel: ObservableObject {
#Published var user : UserProfileModel? = nil // now an optional
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}.first // Take the first document (since there probably should be only one anyway)
}
}
}
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View {
VStack {
if let user = viewModel.user { //only display data if the user isn't nil
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
I'd say a more traditional way of handling this might be to store your user profile document Users/uid/ -- that way you can just user document(uid) to find it rather than the whereField query.