Display data from firestore - firebase

I am trying to display the data I get back from the firestore in my swiftUI app but am having some trouble.
Here is where I get the data
import Foundation
import Firebase
class RecipesViewModel: ObservableObject {
#Published var userRecipes = [RecipesData]()
func getData() {
// Get a reference to the database
let db = Firestore.firestore()
// Read the documents at a specific path
db.collection("Recipes").getDocuments { snapshot, error in
// Check for errors
if error == nil {
// No errors
if let snapshot = snapshot {
// Update the list property in the main thread
DispatchQueue.main.async {
self.userRecipes = snapshot.documents.map { d in
return RecipesData(id: d["id"] as? String ?? "", name: d["name"] as? String ?? "", cuisine: d["cuisine"] as? String ?? "", difficulty: d["difficulty"] as? String ?? "", dishType: d["dishType"] as? String ?? "", prepMins: d["prepMins"] as? Int ?? 0, prepHours: d["prephours"] as? Int ?? 0, cookMins: d["cookMins"] as? Int ?? 0, cookHours: d["cookHours"] as? Int ?? 0, restMins: d["restMins"] as? Int ?? 0, restHours: d["restHours"] as? Int ?? 0, likes: d["lkies"] as? Int ?? 0)
}
}
}
}
else {
}
}
}
}
and here is where the data gets stored
import SwiftUI
import FirebaseFirestoreSwift
struct RecipesData: Identifiable{
var id: String
var name: String
var cuisine: String
var difficulty: String
var dishType: String
var prepMins: Int
var prepHours: Int
var cookMins: Int
var cookHours: Int
var restMins: Int
var restHours: Int
var likes: Int
}
I can get the Data and if I choose to display the name of each recipe in a list I can do that.
But what I want to do is to have my data too like this when being displayed
import SwiftUI
struct AllRecipesView: View {
#ObservedObject var model = RecipesViewModel()
private var gridCollum = [GridItem(.flexible(),spacing: 0), GridItem(.flexible(),spacing: 0)]
var body: some View {
VStack{
LazyVGrid(columns: gridCollum, spacing: 0){
ForEach(model.userRecipes) {item in
overlayView()
}
}
}
}
init(){
model.getData()
}
}
struct AllRecipesView_Previews: PreviewProvider {
static var previews: some View {
AllRecipesView()
}
}
struct overlayView:View{
#ObservedObject var model = RecipesViewModel()
var body: some View{
ForEach(model.userRecipes) {item in
VStack{
VStack{
Spacer()
HStack{
HStack{
Image(systemName: "star")
.foregroundColor(.white)
.font(.system(size: 20))
Text(item.likes)
.foregroundColor(.white)
.font(.system(size: 15))
}
.padding(.trailing)
Text(item.prepMins)
.foregroundColor(.white)
.font(.system(size: 15))
.padding(.horizontal)
}
.padding(.bottom)
}
.frame(width:180,height:130)
.background(Color.red)
.cornerRadius(8)
.shadow(color: .black, radius: 3, x: 2, y: 2)
.padding(.bottom)
Text("Salmon and Rice")
Text("Some User")
}
}
}
init(){
model.getData()
}
}
But I keep getting error saying No exact matches in call to initializer on the lines
Text(item.likes)
and
Text(prepMins)
How can I fix my errors please

try something like this example code, using #StateObject var model = RecipesViewModel() and passing it using #EnvironmentObject.
To fix the errors you get, remenber Text() needs a string, such as, Text(String(item.likes)) or Text("\(item.likes)").
struct AllRecipesView: View {
#StateObject var model = RecipesViewModel() // <-- here
private var gridCollum = [GridItem(.flexible(),spacing: 0), GridItem(.flexible(),spacing: 0)]
var body: some View {
VStack{
LazyVGrid(columns: gridCollum, spacing: 0){
ForEach(model.userRecipes) {item in
OverlayView()
}
}
}
.environmentObject(model) // <-- here
.onAppear {
model.getData() // <-- here
}
}
}
struct OverlayView:View{
#EnvironmentObject var model: RecipesViewModel // <-- here
var body: some View{
ForEach(model.userRecipes) {item in
VStack{
VStack{
Spacer()
HStack{
HStack{
Image(systemName: "star")
.foregroundColor(.white)
.font(.system(size: 20))
Text(String(item.likes)) // <-- here
.foregroundColor(.white)
.font(.system(size: 15))
}
.padding(.trailing)
Text(String(item.prepMins)) // <-- here
.foregroundColor(.white)
.font(.system(size: 15))
.padding(.horizontal)
}
.padding(.bottom)
}
.frame(width:180,height:130)
.background(Color.red)
.cornerRadius(8)
.shadow(color: .black, radius: 3, x: 2, y: 2)
.padding(.bottom)
Text("Salmon and Rice")
Text("Some User")
}
}
}
}

Related

Firestore doesn't save data

I am trying to make a simple app that lets users create recipes and then upload them to the firestore
I am using an environment object to store the data until I upload it to the firestore
My problem is that when I check the firestore to see if my data has uploaded, the data there is empty
Here is my recipe class:
import Foundation
class recipe: ObservableObject{
#Published var name: String = ""
#Published var prepMins: Int = 0
#Published var prepHours: Int = 0
#Published var cookMins: Int = 0
#Published var cookHours: Int = 0
#Published var restMins: Int = 0
#Published var restHours: Int = 0
#Published var dishType: String = ""
#Published var cuisine: String = ""
#Published var difficulty: String = ""
#Published var instructions: [String] = []
#Published var ingredients: [String] = []
}
Here is a sample upload view:
import SwiftUI
struct UploadView: View {
#State private var dishType = "Starter"
#State private var cuisine = "European"
#State private var difficulty = "Easy"
#State var name: String = ""
#State private var isPresented = false
#State private var isPresented1 = false
#State var prepHours: Int = 0
#State var prepMins: Int = 0
#State var cookHours: Int = 0
#State var cookMins: Int = 0
#State var restHours: Int = 0
#State var restMins: Int = 0
#StateObject var recipe1 = recipe()
#EnvironmentObject var viewRouter: ViewRouter
var options = ["Easy", "Medium", "Difficult"]
var body: some View {
ScrollView{
VStack{
HStack{
Spacer()
Text("Create a Recipe")
.font(.system(size: 30))
.fontWeight(.bold)
Spacer()
}
.padding(.top)
}
Group{
HStack{
Text("Name")
.padding()
.font(.system(size: 25, weight: .medium))
Spacer()
}
HStack{
TextField("Enter a name", text: $name)
.padding()
.zIndex(1)
Spacer()
}
}
Group{
HStack{
Text("Choose a dish type")
.padding()
Spacer()
}
HStack{
Button(dishType) {
isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented) {
SelectionView()
}
Spacer()
}
.padding(.horizontal)
HStack{
Text("Choose a cuisine")
.padding()
Spacer()
}
HStack{
Button(cuisine) {
isPresented1.toggle()
}
.fullScreenCover(isPresented: $isPresented1) {
SelectionView1()
}
Spacer()
}
.padding(.horizontal)
}
HStack{
Text("Choose a difficulty")
.padding()
Spacer()
}
Picker("Diffuculty", selection: $difficulty) {
ForEach(options, id: \.self) {
Text($0)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
.padding(.bottom)
Group{
VStack{
HStack{
Text("Prep Time")
.fontWeight(.medium)
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
HStack{
Text("How long does it take to make the dish")
.fontWeight(.light)
.padding(.horizontal)
Spacer()
}
}
HStack{
HStack{
VStack {
Picker("", selection: $prepHours){
ForEach(0..<12, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}
}
VStack {
Picker("", selection: $prepMins){
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
}
}
}
}
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
VStack{
VStack{
HStack{
Text("Cook Time")
.fontWeight(.medium)
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
HStack{
Text("How long does it take to cook the dish")
.fontWeight(.light)
.padding(.horizontal)
Spacer()
}
}
}
HStack{
HStack{
VStack {
Picker("", selection: $cookHours){
ForEach(0..<12, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}
}
VStack {
Picker("", selection: $cookMins){
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
.foregroundColor(.black)
}
}
}
}
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
VStack{
VStack{
HStack{
Text("Rest Time")
.fontWeight(.medium)
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
HStack{
Text("How long does the dish need to rest")
.fontWeight(.light)
.padding(.horizontal)
Spacer()
}
}
}
HStack{
HStack{
VStack {
Picker("", selection: $restHours){
ForEach(0..<12, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}
}
VStack {
Picker("", selection: $restMins){
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
.foregroundColor(.black)
}
}
}
}
.padding(.horizontal)
.padding(.bottom)
Spacer()
}
}
Button {
//change view router
//add data to data class
recipe1.name = name
recipe1.dishType = dishType
recipe1.cuisine = cuisine
recipe1.difficulty = difficulty
recipe1.prepMins = prepMins
recipe1.prepHours = prepHours
recipe1.cookMins = cookMins
recipe1.cookHours = cookHours
recipe1.restMins = restMins
recipe1.restHours = restHours
viewRouter.currentPage = .uploadView2
} label: {
Label("next", systemImage: "arrow.right")
}
.padding()
.frame(width: 100)
.foregroundColor(Color.white)
.background(Color.red)
.cornerRadius(8)
}
.environmentObject(recipe1)
}
}
struct UploadView_Previews: PreviewProvider {
static var previews: some View {
UploadView()
.previewDevice(PreviewDevice(rawValue: "iPhone 13"))
UploadView()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
}
}
And here is my final view with the upload function
import SwiftUI
import Firebase
struct UploadView3: View {
#EnvironmentObject var viewRouter: ViewRouter
#StateObject var recipe1 = recipe()
var body: some View{
ScrollView{
VStack{
HStack{
Button {
print("Going Back")
viewRouter.currentPage = .uploadView2
} label: {
Image(systemName: "arrow.left")
.font(.system(size: 30))
.foregroundColor(.black)
}
.padding(.horizontal)
Spacer()
Text("Add Instructions")
.font(.system(size: 30))
.fontWeight(.bold)
Spacer()
Button {
print("Saved")
} label: {
Image(systemName: "bookmark")
.font(.system(size: 30))
.foregroundColor(.black)
}
.padding()
}
Text("Add Instructions so that people can easily follow your recipe")
.padding()
Button {
self.addData()
} label: {
Label("next", systemImage: "arrow.right")
}
}
}
.environmentObject(recipe1)
}
func addData(){
let db = Firestore.firestore()
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else{return}
db.collection("Recipes").addDocument(data: ["id" : uid, "name" : recipe1.name, "dishType" : recipe1.dishType, "cuisine" : recipe1.cuisine, "difficulty" : recipe1.difficulty,"prepMins" : recipe1.prepMins, "prepHours": recipe1.prepHours, "cookMins" : recipe1.cookMins, "cookHours": recipe1.cookHours, "restMins" : recipe1.restMins, "restHours" : recipe1.restHours, "ingredients" : recipe1.ingredients])
{ err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
}
struct UploadView3_Previews: PreviewProvider {
static var previews: some View {
UploadView3()
}
}
if there is a better way than using an environment object please let me know, otherwise How can I pass the data between my views?
Many Thanks for your time
you have two independent #StateObject var recipe1 = recipe(), one in UploadView and in UploadView3. So when you use func addData(){...} in UploadView3, you have a new empty recipe.
You should have only one, that is the single source of truth. Then use
#ObservedObject var recipe1: recipe or .environmentObject(recipe1) and #EnvironmentObject var recipe1: recipe to pass it around.

Update toggle SwiftUI toggle switch with Firebase

Despite the simplicity of the question I have not been able to find a satisfactory answer yet. I want to update toggle switches based on value in Firebase. I have added listeners but run into problems converting a Bool to Binding Bool, any help is appreciated.
struct oneSeqeuncer : Identifiable{
var id: String
var status: Bool
}
struct Sequencers: View {
#ObservedObject var seqModel = SequencerModel()
#State private var novaseq404A :Bool = true
#State private var novaseq404B :Bool = true
#State private var novaseq297A :Bool = true
#State private var novaseq297B :Bool = true
var body: some View {
ZStack{
VStack{
Text("Sequencers")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
.padding()
List{
HStack{
Text("404")
.font(.title)
.padding()
Toggle("", isOn: $novaseq404A)
.onChange(of: novaseq404A) { newValue in
updateStatus(name: "404A", status: novaseq404A)
}
Toggle("", isOn: $novaseq404B)
.padding()
.onChange(of: novaseq404B) { newValue in
updateStatus(name: "404B", status: novaseq404B)
}
}
HStack{
Text("297")
.font(.title)
.padding()
Toggle("", isOn: $novaseq297A)
.onChange(of: novaseq297A) { newValue in
updateStatus(name: "297A", status: novaseq297A)
}
Toggle("", isOn: $novaseq297B)
.padding()
.onChange(of: novaseq297B) { newValue in
updateStatus(name: "297B", status: novaseq297B)
}
}
}
}
}.onAppear(){
self.seqModel.fetchData()
for seq in seqModel.seqs{
if seq.id == "404A"{
novaseq404A = seq.status
}
if seq.id == "404B"{
novaseq404A = seq.status
}
if seq.id == "297A"{
novaseq297A = seq.status
}
if seq.id == "297B"{
novaseq297B = seq.status
}
}
func updateStatus(name: String, status: Bool){
let timeInterval = NSDate().timeIntervalSince1970
let myInt = Int(timeInterval)
let db = Firestore.firestore()
if status == false{
db.collection("Sequencers").document(name).updateData([
"status": false,
"lastChange" : myInt
]){ error in
if error != nil{
print(error!)
}
}
}
else{
let docRef = db.collection("Sequencers").document(name)
docRef.getDocument {(document, error) in
if error != nil {
print(error!)
}
if let document = document, document.exists{
let data = document.data()
if let lastChange = data!["lastChange"]! as? Int{
let timeOff = myInt - lastChange
if let timeOffTotal = data!["timeOff"]! as? Int{
let newTimeOff = timeOffTotal + timeOff
db.collection("Sequencers").document(name).updateData([
"timeOff" : newTimeOff
])
}
}
db.collection("Sequencers").document(name).updateData([
"previousChange": data!["lastChange"]!,
"status": true ,
"lastChange" : myInt
])
}
}
}
}
}
struct Sequencers_Previews: PreviewProvider {
static var previews: some View {
Sequencers()
}
}
Below is my model for storing 'sequencers'
import Foundation
import FirebaseFirestore
import Firebase
class SequencerModel : ObservableObject {
#Published var seqs = [oneSeqeuncer]()
private var db = Firestore.firestore()
func fetchData(){
db.collection("Sequencers").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.seqs = documents.map { queryDocumentSnapshot -> oneSeqeuncer in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let status = data["status"] as? Bool
print(id)
print(status as Any)
return oneSeqeuncer(id: id, status: status!)
}
}
}
}
My solution was not ideal but solved, I realized the function .fetchData() that I was calling to was taking too long to respond. Ideally I should use some completion handler... However I simply changed my TabView on ContentView to display another page first, to allow time for my call to Firebase to finish, which allowed my for loop in .onAppear to have a non empty iterable. Again not sure this really belongs as an "Answer" but just wanted to share my temp solution as an option.

Abrupt transition between views within a Navigation View

I'm having trouble figuring out how to smoothly navigate from my SignInView() to my FirstView(). I have my FirstView() inside of a Navigation Stack, but the transition between the views is very abrupt and devoid of the transition that you normally get with the use of a NavigationLink. How can I get the transition to work?
Much appreciated!
Here is the relevant code...
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
VStack{
NavigationView {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.navigationBarHidden(true)
}
}
.onAppear {
viewModel.listen()
}
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
withAnimation {
self.signedIn = true
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
}
struct SignInView: View {
#EnvironmentObject var viewModel: AppViewModel
#State private var username : String = ""
#State private var password : String = ""
#State private var shouldShowLoginAlert: Bool = false
#State var selectedImageArray : [Image] = []
var disableLoginButton : Bool {
return self.username.isEmpty || self.password.isEmpty
}
var body: some View {
VStack{
Image(uiImage: #imageLiteral(resourceName: "awText"))
.resizable()
.frame(width: 180, height: 100)
.padding(.bottom, 50)
TextField("Email", text: $username)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center).padding(.bottom)
.padding(.bottom)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(20)) //Max 10 Characters for Username.
}
})
SecureField("Password", text: $password)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(10)) //Max 10 Characters for Password.
}
})
//SignIn Button
Button(action: {
viewModel.signIn(email: username, password: password)
}, label: {
Text("Sign In")
.disabled(disableLoginButton)
.frame(width: 300, height: 50)
.background(Color.green)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.padding()
})
}
Replacing the default NavigationView behavior with your own animations isn't necessarily totally straightforward. I'll lay out one possibility, but another would be to use a real NavigationView transition, but just hide the back button once you're on FirstView.
To do the transition yourself, you'll need one root element to NavigationView, an if clause, a transition(.slide) and withAnimation. Here's a simplified version of your code showing just these elements:
class AppViewModel: ObservableObject {
#Published var signedIn = false
}
struct FirstView : View {
var body: some View {
Text("Signed in")
}
}
struct ContentView: View {
#StateObject var viewModel = AppViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
Button("Sign me in") {
withAnimation {
viewModel.signedIn = true
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarHidden(true)
}
}
}

Why does the Navigation Bar from the View still remain after Signing Out a User

I'm working on a very simple app to try to wrap my mind around SwiftUI and Firebase. I have a Login View, a SignUp View, and the FirstView that gets presented once the user is logged in.
Once a User is logged in and the SignOut button is tapped, the Navigation Bar that was only visible in the FirstView, all of a sudden appears superimposed on the LoginView as well. That's not supposed to happen and I'm trying to understand why that happens. Perhaps I'm not dismissing the FirstView correctly?
As a side question, I'm also running into an issue where my User Data is presented/Updated on the FirstView after Signing In, but with a noticeable delay between the Text change for the fields within the view.
Any help would be greatly appreciated.
Here is the relevant code...
#main
struct AWSupportLoggerApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var viewModel = AppViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
class AppDelegate:NSObject,UIApplicationDelegate{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
self.signedIn = true
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
func signOut(){
do {
try authRef.signOut()
} catch {
print(error)
}
}
func signUp(email: String, password: String, company: String, name: String, admin: Bool, photo: String){
authRef.createUser(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
let db = Firestore.firestore()
//Success
db.collection("Users").document("\(result!.user.uid)").setData(["company" : "\(company)", "name" : "\(name)", "admin" : admin, "photo" : "\(photo)", "uid":result!.user.uid]) { error in
if error != nil {
print(error!)
}
}
}
}
func unbind() {
if let handle = handle {
authRef.removeStateDidChangeListener(handle)
}
}
}
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView {
if viewModel.signedIn {
FirstView()
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
}
}
.onAppear {
viewModel.listen()
}
}
}
struct FirstView: View {
#EnvironmentObject private var appViewModel: AppViewModel
var body: some View {
VStack{
Spacer()
VStack(spacing: 50){
NavigationLink(destination: Text("Test")){
awButton(content: "Request Support", backColor: Color(#colorLiteral(red: 0, green: 0.723585546, blue: 0.9907287955, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Request Quote", backColor: Color(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Ticket Status", backColor: Color(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
}
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle(appViewModel.userInfo?.company ?? "Test")
.navigationBarItems(leading: Button(action: {
appViewModel.signOut()
}) {
HStack {
Text("Sign Out")
}
},trailing: HStack{
Image(systemName: "bell")
Text(appViewModel.userInfo?.name ?? "Tester")
.font(.system(size: 20))
})
}
}
Try to hide it explicitly, like
if viewModel.signedIn {
FirstView()
.navigationBarHidden(true) // << here !!
} else {
Hiding the navigationBar will cause lots of problems if you need it in other views .the better answer in my opinion is to
1 - create a splash view and set it as your starting view in #main file
2 - add a StateObject for tracking user loginState :
class LoginState: ObservableObject {
#Published var isLogin : Bool = false
}
3 - add navigationView to splash view
4 - pass loginState , StateObject as .environmentObject(loginState) to navigationView
5- now you can change login state when ever you want from anywhere without need of hiding the navigation bar

Slight delay/lag when fetching Firestore User data to update View

I'm currently building a simple app with a SignIn/SignUp View in order to better understand how to integrate SwiftUI with Firebase.
I've run into a bit of an issue when I try to update User Data. I noticed that after Signing Out as one User and then Signing In as a different User, there is a split second delay before the User information is updated on the FirstView after the Sign In button is tapped. I'm trying to figure out how to avoid this delay from happening.
As a side, I'm also having trouble figuring out how to smoothly navigate from my LoginView() to my FirstView(). I have my FirstView() inside of a Navigation Stack, but the transition between the views is very abrupt and devoid of the NavigationLink animation. How can I correct this issue?
Much appreciated!
Here is the relevant code...
#main
struct AWSupportLoggerApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#StateObject var viewModel = AppViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
class AppDelegate:NSObject,UIApplicationDelegate{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
}
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView {
if viewModel.signedIn {
FirstView()
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.navigationBarHidden(true)
}
}
.onAppear {
viewModel.listen()
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
self.signedIn = true
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
func signOut(){
do {
try authRef.signOut()
} catch {
print(error)
}
}
func signUp(email: String, password: String, company: String, name: String, admin: Bool, photo: String){
authRef.createUser(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
let db = Firestore.firestore()
//Success
db.collection("Users").document("\(result!.user.uid)").setData(["company" : "\(company)", "name" : "\(name)", "admin" : admin, "photo" : "\(photo)", "uid":result!.user.uid]) { error in
if error != nil {
print(error!)
}
}
}
}
func unbind() {
if let handle = handle {
authRef.removeStateDidChangeListener(handle)
}
}
}
struct FirstView: View {
#EnvironmentObject private var appViewModel: AppViewModel
var body: some View {
VStack{
Spacer()
VStack(spacing: 50){
NavigationLink(destination: Text("Test")){
awButton(content: "Request Support", backColor: Color(#colorLiteral(red: 0, green: 0.723585546, blue: 0.9907287955, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Request Quote", backColor: Color(#colorLiteral(red: 0.9372549057, green: 0.3490196168, blue: 0.1921568662, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
NavigationLink(destination: Text("Test")){
awButton(content: "Ticket Status", backColor: Color(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)))
.shadow(color: Color.primary.opacity(0.5), radius: 20, x: 0, y: 20)
.rotation3DEffect(Angle(degrees:10), axis: (x: 10.0, y: 0, z: 0))
}
}
Spacer()
}
.navigationBarBackButtonHidden(true)
.navigationTitle(appViewModel.userInfo?.company ?? "Test")
.navigationBarItems(leading: Button(action: {
appViewModel.signOut()
}) {
HStack {
Text("Sign Out")
}
},trailing: HStack{
Image(systemName: "bell")
.font(.system(size: 30))
// selectedImageArray.first!
// .resizable()
// .scaledToFit()
// .clipShape(Circle())
// .frame(width: 50, height: 50)
Text(appViewModel.userInfo?.name ?? "Tester")
.font(.system(size: 20))
})
}
}
Your navigation state depends on signedIn. In your auth listener, you do this:
DispatchQueue.main.async {
self.fetchUserData()
}
self.signedIn = true
This will set signedIn to true, which will change your navigation state and then at an indeterminate time in the future, fetchUserData will finish and update the user data (once the network call has completed).
To avoid this delay (or, really, to just avoid seeing the information not update on the screen -- the delay is an inevitability of the network call), don't set signedIn until fetchUserData completes. So, remove the line inside the listener and instead, set it after your self.userInfo = line.

Resources