How Can You Reinstantiate EnvironmentObjects is SwiftUI? - swift

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?

Related

How do I show a certain amount of of users while giving the option to load more?

Part of my app, I allow users to explore other users with a ForEach line - however I have quickly realised how inefficient and laggy this will become in the future.
My question is: How do I only show a few users at first (lets say 8) then once you scroll to the bottom of the displayed users more will load in?
My code is below.
SWIFT UI VIEW:
NavigationView {
VStack {
SearchBar(text: $viewModel.searchText)
.padding()
ScrollView {
LazyVStack {
ForEach(viewModel.searchableUsers) { user in
NavigationLink {
ProfileView(user: user)
} label: {
UserRowView(user: user)
}
}
}
}
}
}
VIEW MODEL:
class ExploreViewModel: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
private let config: ExploreViewModelConfiguration
var searchableUsers: [User] {
if searchText.isEmpty {
return users
} else {
let lowercasedQuery = searchText.lowercased()
return users.filter({
$0.username.contains(lowercasedQuery) ||
$0.name.lowercased().contains(lowercasedQuery)
})
}
}
let service = UserService()
init(config: ExploreViewModelConfiguration) {
self.config = config
fetchUsers(forConfig: config)
}
func fetchUsers(forConfig config: ExploreViewModelConfiguration) {
service.fetchUsers { users in
let users = users
switch config {
case .newMessage:
self.users = users.filter({ !$0.isCurrentUser })
case .search:
self.users = users
}
}
}
}

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.

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.

Should I always manually detach the Firestore listener?

I'm using Firestore with SwiftUI. I'm not sure how to handle the lister to avoid the unnecessary reads from Firestore.
When I tap Text("Books"), pop from BooksView, and then tap Text("Books") again, if I didn't call listener?.remove() within deinit, Will I have two listeners or only one listener? Will the cost also double or not?
In other words, the listener is smart enough to detach the same listener when calling .addSnapshotListener second time?
Should I alway manually detach the lister like calling listener?.remove() within deinit?
How about terminating the app without detach the listener, opening it, and attach the new listener?
Sample Code:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: BooksView()) {
Text("Books")
}
}
}
}
struct BooksView: View {
#StateObject var booksViewModel = BooksViewModel()
var body: some View {
Text("Hello, world!")
.padding()
}
}
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
private var listener: ListenerRegistration?
init() {
fetchData()
}
func fetchData() {
listener = db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.map { queryDocumentSnapshot -> Book in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let author = data["author"] as? String ?? ""
let numberOfPages = data["pages"] as? Int ?? 0
return Book(id: .init(), title: title, author: author, numberOfPages: numberOfPages)
}
}
}
}
Is this necessary to avoid unnecessary read?
deinit {
listener?.remove()
}
In other words, the listener is smart enough to detach the same listener when calling .addSnapshotListener second time?
No, it's not. You have to manually remove any listeners when they are no longer needed.
How about terminating the app without detach the listener, opening it, and attach the new listener?
If the app process terminates, then the listener will obviously go away (because the program isn't running). You will have to add a new listener when the app starts again, if you want more updates.

Display query values within view's body

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