Swiftui - Fetch data after login - swift

I'm starting to learn SwiftUI and i'm looking for better solution to fetch user data after successful login.
For example, i need to see if the user has the account blocked or not after login
For this i have create struct in Users.swift
struct Users: Identifiable, Codable{
#DocumentID var id: String?
var Societe : String
var Nom : String
var Prenom : String
var HasBlocked : Bool
var SiteID : [String]
var Poste : String
var Email : String
}
and firebase class in Firebase.swift
class Firebase: ObservableObject {
#EnvironmentObject var viewRouter: ViewRouter
#Published var user : Users? = nil
private var db = Firestore.firestore()
func fetchData() {
db.collection("Users").document(Auth.auth().currentUser!.uid).addSnapshotListener{ (snapshot, error) in
guard let documents = snapshot?.data() else {
print("No documents")
return
}
let name = documents["Nom"] as? String ?? ""
let surname = documents["Prenom"] as? String ?? ""
let email = documents["Email"] as? String ?? ""
let societe = documents["Societe"] as? String ?? ""
let poste = documents["Poste"] as? String ?? ""
let siteId = documents["SiteID"] as? [String] ?? []
let hasBlocked = documents["HasBlocked"] as? Bool ?? true
if let user = self.user?.SiteID{
print(user)
}else{
print("no document2")
}
self.user = Users(Societe: societe, Nom: name, Prenom: surname, HasBlocked: hasBlocked, SiteID: siteId, Poste: poste, Email: email)
}
}
}
and in the SignInView after press login button, i call func signInUser
#ObservedObject private var firebase = Firebase()
func signInUser(userEmail: String, userPassword: String) {
signInProcessing = true
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
guard error == nil else {
signInProcessing = false
signInErrorMessage = error!.localizedDescription
return
}
switch authResult {
case .none:
print("Could not sign in user.")
signInProcessing = false
case .some(_):
print("User signed in")
// get data and check ??
signInProcessing = false
withAnimation {
viewRouter.currentPage = .homePage
}
}
}
}
But i don't know how to fetch data after login for check if the account has blocked and then pass the data to Homeview without re-fetch data to optimize call database.
thank for your help

Let me address this at a high level as your code seems fine but its's just wrapping your brain around the asynchronous calls.
I am pretty sure you have everything you need - here's the flow using pseudo code
authenticate { auth closure
read user document using auth.uid { snapshot closure
if snapshot has user blocked {
do not proceed to next view
} else { //user is not blocked
proceed to next view
}
}
}
Remember that Firebase data is only valid within the closure following the Firebase call and if you follow the above pseudo code, it does just that...
auth closure - has valid firebase auth information so you can get the uid
snapshot closure - has valid snapshot info so you can see if the user is blocked
the If statement will then determine the next thing to do - either don't show
the view if blocked or show it if not blocked
So borrowing from your code
func signInUser(userEmail: String, userPassword: String) {
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
db.collection("Users").document(Auth.auth().currentUser!.uid).getDocument {
either go to next view or not
Note that you should be using getDocument in this case to read the document once instead of adding a listener.

Related

Slight delay when retrieving from Firestore

When a user first logs into their profile, I retrieve there user name and profile picture. My issue is the site loads and firebase takes around a second to load the information. For example, there username will flash "unavailable" for a brief moment, before displaying the name.
Would love to get feedback on how to better improve my process of retrieving the information. Thank you! For the sake of less code, I didn't include my profile picture logic, as I'm guessing my issue has to do with the way I'm calling Firebase in the first place in my dashboard logic class.
struct UserDashController: View {
#ObservedObject var vm = DashboardLogic()
#State private var action: Int? = 0
#State private var userSigningOut = false
#State private var showMenu = false
#State private var presentSettingsPage = false
var body: some View {
NavigationView{
VStack{
HStack{
//retrieve username
Text(vm.userModel?.name ?? "Name Unavailable" )
}
.padding()
}
.padding(.top, -5)
}
}
Dashboard Logic
class DashboardLogic: ObservableObject {
#Published var userModel: UserModel?
#Published var privateUserModel: privateUserModel?
init(){
fetchCurrentUser()
}
private func fetchCurrentUser () {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
return
}
guard let email = FirebaseManager.shared.auth.currentUser?.email else {
print("could not locate email")
return
}
FirebaseManager.shared.firestore
.collection("users").document(uid)
.getDocument { snapshot, error in
if let error = error {
print ("failed to fetch user \(error)")
return
}
guard let data = snapshot?.data() else {
print ("no data found for user")
return
}
self.userModel = .init(data: data)
}
//save to private database
FirebaseManager.shared.firestore
.collection("users").document(uid)
.collection("privateUserInfo")
.document("private")
.getDocument { snapshot, error in
if let error = error {
print("oh no we messed up")
return
}
//save snapshot of database from firestore
guard let userEmail = snapshot?.data() else {
print("no email found for user")
return
}
self.privateUserModel = .init(data:userEmail )
}
}
}
USER MODEL
struct UserModel {
var uid, name, gender, height, weight, agenda, profilePictureURL: String
init(data: [String: Any]){
self.uid = data["uid"] as? String ?? "Unavailable"
self.name = data["name"] as? String ?? "Unavailable"
self.gender = data["gender"] as? String ?? "Unavailable"
self.height = data["height"] as? String ?? "Unavailable"
self.weight = data["weight"] as? String ?? "Unavailable"
self.agenda = data["agenda"] as? String ?? "Unavailable"
self.profilePictureURL = data ["profilePicture"] as? String ?? "Unavailable"
}
}
struct privateUserModel {
var email: String
init(data: [String: Any]){
self.email = data["email"] as? String ?? "Unavailable"
}
}
The only I would change is to update published properties on main queue, like
DispatchQueue.main.async {
self.userModel = .init(data: data)
}
// ...
DispatchQueue.main.async {
self.privateUserModel = .init(data:userEmail )
}
everything else I assume is just network delays.
Update
It’s not predictable for how long the latency will last, but you could implement loading animation features while waiting for the data to be fetched from Cloud Firestore instead of having just a simple “Unavailable” message, like in this post.
There are also some ways to handle loading states within SwiftUI views as described here, from self-loading views, view models to connecting Combine publishers to views and supporting custom loading views.
On the other hand, you could use an activity indicator and try a view called ProgressView as described in this other post.
As a complement and from a Cloud Firestore configuration perspective, you can improve your process of retrieving data following the best practices such as selecting the database location closest to your users and compute resources. Far-reaching network hops are more error-prone and increase query latency.
You can also follow these practices about the use of asynchronous methods that can reduce your delays as completion handlers or using async/await.
Finally, consider that the delay can be generated for external conditions in the network itself.
You may also be interested in looking into this other related question.

get the Field datas according to the Ids from the Collection in the Firestore/SwiftUI

so, the problem that need to be solved. we need to be able to get the data of any user(Field) in the "users"-(Collection) according to the its ID
these are my attempts:
FriendProViewModel() :
#Published var uid: String
#Published var displayName: String
#Published var email: String
init(uid: String, displayName: String, email: String) {
self.uid = uid
self.displayName = displayName
self.email = email
}
And to check the result:
#State var data = [FriendProViewModel]()
let db = Firestore.firestore()
var body: some View {
VStack {
ForEach((self.data), id: \.self.uid) { item in
Text("\(item.displayName)")
Text("\(item.email)")
}
}.onAppear {
// self.fetchData()
self.fetchData2()
}
}
func fetchData() {
// Remove previously data to prevent duplicate data
self.data.removeAll()
self.db.collectionGroup("users").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
let uid = document.documentID
let displayName = document.get("displayName") as! String
let email = document.get("email") as! String
self.data.append(FriendProViewModel(uid: uid, displayName: displayName, email: email))
}
}
}
}
func fetchData2() {
db.collection("users").document("pJSsQQ2qt6Xx9qqRGpVzRfWLgC33").getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
}
Here is the result I am getting for fetchData():
Here is the result I am getting for fetchData2():
fetchData() retrieves all data from all "users". fetchData2() is only getting data according to the id I gave.
separately, I was unable to get the data of each according to the id-s...
I hope everyone understood the question. thank you for your attention ...
You are not setting up and using FriendProViewModel correctly.
Read-up on how to use ObservableObject.
Re-structure your code to use a ObservableObject and its constituent structs, such as:
// a struct to hold each individual info
struct FriendModel {
var uid: String
var displayName: String
var email: String
}
// an observable, to let the view know when it changes
class FriendProViewModel: ObservableObject {
#Published var friends: [FriendModel] = []
}
Declare it like this in your View:
#StateObject var viewModel = FriendProViewModel() // instead of `data`
and use it like this:
viewModel.friends.append(FriendModel(uid: uid, displayName: displayName, email: email))
and
ForEach(viewModel.friends, id: \.uid) { item in ...}
I also suggest you move the fetchData() etc... inside the FriendProViewModel

How do I read a User's Firestore Map to a Swift Dictionary?

I have my user struct with has a dictionary of all their social medias.
struct User: Identifiable {
var id: String { uid }
let uid, email, name, bio, profileImageUrl: String
let numSocials, followers, following: Int
var socials: [String: String]
init(data: [String: Any]) {
self.uid = data["uid"] as? String ?? ""
self.email = data["email"] as? String ?? ""
self.name = data["name"] as? String ?? ""
self.bio = data["bio"] as? String ?? ""
self.profileImageUrl = data["profileImageURL"] as? String ?? ""
self.numSocials = data["numsocials"] as? Int ?? 0
self.followers = data["followers"] as? Int ?? 0
self.following = data["following"] as? Int ?? 0
self.socials = data["socials"] as? [String: String] ?? [:]
}
}
The idea is for socials (the dictionary), to be dynamic, since users can add and remove social medias. Firestore looks like this:
The dictionary is initialized as empty. I have been able to add elements to the dictionary with this function:
private func addToStorage(selectedMedia: String, username: String) -> Bool {
if username == "" {
return false
}
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
print("couldnt get uid")
return false
}
FirebaseManager.shared.firestore.collection("users").document(uid).setData([ "socials": [selectedMedia:username] ], merge: true)
print("yoo")
return true
}
However I can't seem to read the firestore map into my swiftui dictionary. I want to do this so that I can do a ForEach loop and list all of them. If the map is empty then the list would be empty too, but I can't figure it out.
Just in case, here is my viewmodel.
class MainViewModel: ObservableObject {
#Published var errorMessage = ""
#Published var user: User?
init() {
DispatchQueue.main.async {
self.isUserCurrentlyLoggedOut = FirebaseManager.shared.auth.currentUser?.uid == nil
}
fetchCurrentUser()
}
func fetchCurrentUser() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
self.errorMessage = "Could not find firebase uid"
print("FAILED TO FIND 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
}
guard let data = snapshot?.data() else {
print("no data found")
self.errorMessage = "No data found"
return
}
self.user = .init(data: data)
}
}
}
TLDR: I can't figure out how to get my firestore map as a swiftui dictionary. Whenever I try to access my user's dictionary, the following error appears. If I force unwrap it crashes during runtime. I tried to coalesce with "??" but I don't know how to make it be the type it wants.
ForEach(vm.user?.socials.sorted(by: >) ?? [String:String], id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
error to figure out
Please be patient. I have been looking for answers through SO and elsewhere for a long time. This is all new to me. Thanks in advance.
This is a two part answer; Part 1 addresses the question with a known set of socials (Github, Pinterest, etc). I included that to show how to map a Map to a Codable.
Part 2 is the answer (TL;DR, skip to Part 2) so the social can be mapped to a dictionary for varying socials.
Part 1:
Here's an abbreviated structure that will map the Firestore data to a codable object, including the social map field. It is specific to the 4 social fields listed.
struct SocialsCodable: Codable {
var Github: String
var Pinterest: String
var Soundcloud: String
var TikTok: String
}
struct UserWithMapCodable: Identifiable, Codable {
#DocumentID var id: String?
var socials: SocialsCodable? //socials is a `map` in Firestore
}
and the code to read that data
func readCodableUserWithMap() {
let docRef = self.db.collection("users").document("uid_0")
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: UserWithMapCodable.self)
print(user.socials) //the 4 socials from the SocialsCodable object
}
}
}
Part 2:
This is the answer that treats the socials map field as a dictionary
struct UserWithMapCodable: Identifiable, Codable {
#DocumentID var id: String?
var socials: [String: String]?
}
and then the code to map the Firestore data to the object
func readCodableUserWithMap() {
let docRef = self.db.collection("users").document("uid_0")
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: UserWithMapCodable.self)
if let mappedField = user.socials {
mappedField.forEach { print($0.key, $0.value) }
}
}
}
}
and the output for part 2
TikTok ogotok
Pinterest pintepogo
Github popgit
Soundcloud musssiiiccc
I may also suggest taking the socials out of the user document completely and store it as a separate collection
socials
some_uid
Github: popgit
Pinterest: pintepogo
another_uid
Github: git-er-done
TikTok: dancezone
That's pretty scaleable and allows for some cool queries: which users have TikTok for example.

SwiftUI App can't get data from Firestore

I will start by explaining with words, then I will explain with code below. Here we go.
I have a struct 'User' that I use in my app. It has important data like username, email, and profileImageUrl. To access a list of users, I need to request a list of users from Firestore. But when I do this, it returns an empty array.
EXCEPT right after I log in. If I check the Users array before I log in, it's empty. If I check the users array after I log in, it's empty. But if I print it inside the .onAppear of the main content view that loads, it prints the true array of users.
I also notice that my program never thinks I'm the current user, even if I am. Each instance of User has a property isCurrentUser: Bool that is supposed to check if the user that is currently logged in is the same user in that instance. But they all appear as false.
It seems to me that the program thinks I'm not logged in, and will only let me pull data from the server if I'm logged in. Even though I changed my Firestore and Firebase Storage rules to allow me to read and write even if I'm not logged in.
Here are important snippets of code.
User struct
struct User : Identifiable {
let id : String
let username : String
let profileImageUrl : String
let email : String
let isCurrentUser: Bool
init (dictionary: [String: Any]) {
self.id = dictionary["uid"] as? String ?? ""
self.username = dictionary["username"] as? String ?? ""
self.profileImageUrl = dictionary["profileImageUrl"] as? String ?? ""
self.email = dictionary["email"] as? String ?? ""
self.isCurrentUser = Auth.auth().currentUser?.uid == self.id
}
}
notice the self.isCurrentUser code. I'm thinking maybe this code isn't working, or that self.id isn't working properly.
I also receive these two errors whenever I unsuccessfully fetch from the Users array.
2021-02-24 21:00:50.172361-0800 Council[38163:1564633] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
2021-02-24 21:00:52.168254-0800 Council[38163:1564630] [connection] nw_resolver_start_query_timer_block_invoke [C1] Query fired: did not receive all answers in time for firebaselogging-pa.googleapis.com:443
So please share any knowledge you have this problem is killing me i have to finish this app in time and now I am not able to work due to this bug
Here is my main ContentView that shows login screen if a user isn't logged in, but shows the homepage if a user is logged in. I called an instance of SearchViewModel() to test if I could print SearchViewModel().users. This is the view in which I can get the users array to print, but only right after I log in. If I attempt to print it on a subsequent page, it'll be the same empty array.
import SwiftUI
struct ContentView: View {
#State private var navBarHidden: Bool = true
#EnvironmentObject var viewModel : AuthViewModel
#ObservedObject var searchViewModel = SearchViewModel()
var body: some View {
Group{
if viewModel.userSession != nil{
NavigationView{
TabView{
FeedView(navBarHidden: $navBarHidden)
.navigationBarHidden(self.navBarHidden)
.tabItem{
Image(systemName: "house")
Text("For You")
}
Text("Random")
.tabItem{
Image(systemName: "questionmark.circle")
Text("Random")
}
SearchView()
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
.navigationBarHidden(true)
}
.accentColor(.black)
.onAppear() {
searchViewModel.fetchUsers()
print(searchViewModel.users)
}
}
} else {
LoginView()
}
}
}
}
More code: Here is my User struct
import Foundation
import Firebase
struct User : Identifiable {
let id : String
let username : String
let profileImageUrl : String
let email : String
let isCurrentUser: Bool
init (dictionary: [String: Any]) {
self.id = dictionary["uid"] as? String ?? ""
self.username = dictionary["username"] as? String ?? ""
self.profileImageUrl = dictionary["profileImageUrl"] as? String ?? ""
self.email = dictionary["email"] as? String ?? ""
self.isCurrentUser = Auth.auth().currentUser?.uid == self.id
}
}
Here is the file for the ViewModel that fetches the users
import SwiftUI
import Firebase
class SearchViewModel: ObservableObject {
#Published var users = [User]()
init() {
fetchUsers()
print("usersFetched")
}
func fetchUsers() {
Firestore.firestore().collection("users").getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else {return}
documents.forEach{ document in
let user = User(dictionary: document.data())
self.users.append(user)
}
}
}
Here is the ViewModel that authenticates users, creates users, and logs them in
import SwiftUI
import Firebase
class AuthViewModel: ObservableObject {
#Published var userSession: FirebaseAuth.User?
#Published var isAuthenticating = false
#Published var error: Error?
#Published var user: User?
init() {
userSession = Auth.auth().currentUser
fetchUser()
}
func login(withEmail email : String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error{
print("Failed to upload image. Error: \(error.localizedDescription)")
return
}
self.userSession = result?.user
print("U logged in")
}
}
func registerUser( email : String, password: String, username: String, profileImage: UIImage) {
guard let imageData = profileImage.jpegData(compressionQuality: 0.3) else {return}
let fileName = NSUUID().uuidString
let storageRef = Storage.storage().reference().child(fileName)
storageRef.putData(imageData, metadata: nil) { _, error in
if let error = error{
print("Failed to upload image. Error: \(error.localizedDescription)")
return
}
print("successful upload of photo")
storageRef.downloadURL { url , _ in
guard let profileImageUrl = url?.absoluteString else {return}
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("Failed to Register. Error: \(error.localizedDescription)")
return
}
guard let user = result?.user else {return}
let data = ["email": email,
"username": username.lowercased(),
"profileImageUrl": profileImageUrl,
"uid": user.uid]
Firestore.firestore().collection("users").document(user.uid).setData(data) { _ in
self.userSession = user
print("successfully uploaded user data")
}
print("Successful SIgnup")
}
}
}
}
func signOut() {
userSession = nil
try? Auth.auth().signOut()
}
func fetchUser() {
guard let uid = userSession?.uid else {return}
Firestore.firestore().collection("users").document(uid).getDocument { snapshot, _ in
guard let data = snapshot?.data() else {return}
let user = User(dictionary: data)
}
}
}
EDIT: New possibility for an issue. When the users array successfully prints, it does so in this format:
MyApp.User(id: "kugrbub", username: "user1", profileImageUrl: "https://theimageurl.com", email: "ceo#violent.jewelry", isCurrentUser: false),
When it prints, it's in parentheses, not braces. So is this a reason why it could be failing?
I believe I see what's going wrong. I think that you're misunderstanding what is happening with the asynchronous Firebase methods.
When you call a method like .getDocuments in Firebase, that method does not return instantaneously. Rather, it runs asynchronously and then calls your callback function that you provide when it's finished.
Why does this lead to the result that you're getting? In ContentView, you initialize SearchViewModel by calling SearchViewModel() to set up the property. In the init() of SearchViewModel, fetchUsers() is called, which populates the users array when it completes.
Then, once your login completes, ContentView switches over to your NavigationView/TabView, and onAppear is called. Inside, you call fetchUsers again, which starts running asynchronously. But, because you had already called fetchUsers in the SearchViewModel init, the array already has data in it (keep in mind that unless you explicitly sign out of Firebase, you'll still be signed in from the previous session).
So, how do you solve this?
Remember that all of those calls are asynchronous. Printing the arrays right after calling the fetchUsers method(s) will never get you the values you want *unless they had been populated before.
Restructure your app so that you can respond to the changes in the view model asynchronously. Because you have so much going on, there's not one definitive way to clean everything up. But, you can look into a couple of things:
look up onReceive which you can use to tell when a single publisher on your ObservableObject has changed.
When using Firebase, I like using Auth.auth().addStateDidChangeListener to respond to the auth state. That might help you start to split up your login logic.
Instead of printing after your fetchUsers call, print from inside the Firebase callback functions.
A code sample for you:
class SearchViewModel: ObservableObject {
#Published var users = [User]()
func fetchUsers() {
guard Auth.auth().currentUser?.uid != nil else {
assertionFailure("NOT SAFE TO GET USERS YET -- NOT LOGGED IN")
return
}
Firestore.firestore().collection("users").getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else {return}
print("Received docs:")
print(documents)
documents.forEach{ document in
let user = User(dictionary: document.data())
self.users.append(user)
}
print("USERS:")
print(self.users)
}
}
}
struct ShowUsersView: View {
#ObservedObject var searchViewModel = SearchViewModel()
var body: some View {
VStack {
ForEach(searchViewModel.users, id: \.id) { user in
Text(user.username)
}
}.onAppear {
print("Calling fetchUsers...")
searchViewModel.fetchUsers()
}
}
}

Create an object from firebase database

I'm using firebase to store user records. When a user logs in, I am trying to pull the record and create a user object to pass around amongst the view controllers as opposed to hitting the database multiple times.
class User: NSObject {
var name: String?
var email: String?
}
I have a variable, myUser: User? in my controller and would like to assign the record retrieved from firebase to that variable.
func retrieveUserWith(uid: String) {
Database.database().reference().child("users").child(uid).observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
let user = User()
user.name = dictionary["name"] as? String
user.email = dictionary["email"] as? String
self.myUser = user
}
})
}
Now I understand that the firebase call is asynchronous and I can't directly assign the created user to myUser variable as shown above.
Is there another way to assign user to myUser to avoid hitting the database every time I switch view controllers?
This is not really the correct way to get the info you want. Firebase already offers a sharedInstance of the User.
if let user = Auth.auth().currentUser {
let name = user.displayName // should check that this exists
let email = user.email // should check that this exists
}
Nonetheless, to achieve this the way you are looking to do so:
class User: NSObject {
var name: String
var email: String?
static var sharedInstance: User!
init(name: String, email: String) {
self.name = name
self.email = email
}
}
Calls to firebase are asynchronous, so you should have a completionHandler that will get called when the call is finished:
func retrieveUserWith(uid: String, completionHandler: #escaping () -> ()) {
Database.database().reference().child("users").child(uid).observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
let name = dictionary["name"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
let user: User = User(name: name, email: email)
User.sharedInstance = user
completionHandler()
}
})
}
Then to use the sharedInstance of the user elsewhere in your app you can do the following:
if let user = User.sharedInstance {
// do stuff here
}