Write Snapshot Listener value to variables in Swift - google-cloud-firestore

I am trying to find a way to connect my snapshot listener to specific variables, so that the rating average which is shown to the user is updated after each rating is submitted. Right now, I'm able to write new averages with the most recent submission taken into account to by Firestore Database. I can also see that my listener works by printing to the console, and it pulls the value that was just written each time. I'm trying to take what is written to the console and assign it to my variables at the top of the view structure so that each new submission is submitted on top of the previous submission averages.
struct NewRatingView: View {
var schools = ["North Avenue", "West Village", "Brittain"]
#State private var selectedSchool = "West Village"
#State private var userCurrentRating: CGFloat = 0.0
#State private var userUsualRating: CGFloat = 0.0
#State private var isUserRatingCurrent = true
#State private var averageCurrentRating: CGFloat = 2
#State private var isUserRatingUsual = true
#State private var averageUsualRating: CGFloat = 2
#State private var totalRatingsCurrent = 1
#State private var totalRatingsUsual = 1
// body view code
private func storeRatingInformation() {
// guard let uid = Auth.auth().currentUser?.uid else {return}
let id = selectedSchool
let ratingData = ["location": selectedSchool, "currentRating": averageCurrentRating, "usualRating": averageUsualRating, "totalCurrentRatings": totalRatingsCurrent, "totalUsualRatings": totalRatingsUsual] as [String : Any]
let db = Firestore.firestore()
db.collection("RatingInformation").document(id)
.setData(ratingData) { error in
if let error = error {
print(error)
return
}
}
}
private func snapshotListener() {
let id = selectedSchool
let db = Firestore.firestore()
db.collection("RatingInformation").document(id)
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
if let currentRating = document.get("currentRating") as? CGFloat {
averageCurrentRating = currentRating
print("Testing:")
print(averageCurrentRating)
print(document.data())
}
}
}

Related

Function with a parameter inside init() not updating onAppear{}

Attempting to call a function within init(). I have a function that takes a parameter which I call in the initializer. I set the parameter to a blank string at first until I pass in the value in another view when it appears.
My issue is the function isn't updating immediately when the view first appears. My objective is to just have the function run immediately once the view is generated and have the view update immediately (where I display in my view values from the function).
My guess is since I'm passing in the initial blank string during the init(), my function isn't firing with the updated variable. I don't want to set the #State to #Binding as I don't want to have to pass in a value everytime I call the observedObject. Any advice is greatly appreciated.
To summarize my issue, when I call the grabItems recipe when my view first appears, it doesn't initially get called with the correct parameter (it gets called with a blank string).
class testClass: ObservableObject {
#State var uidNonUser: String
#Published var itemsNonUser = [Item]() // items saved
init(){
self.uidNonUser = ""
grabItems(userUID: uidNonUser) // << grabs things
}
}
struct testStruct: View {
#ObservedObject var rm = testClass()
#State var userUID: String // << variable passing to grabItems
var body: some View {
Text(rm.itemsNonUser.count)
.onAppear{
rm.grabItems(userUID: userUID)
}
}
}
FYI - pasting my actual grabItems recipe below just as reference in case it helps understand the issue.
func grabItems(userUID: String){
//grab user thats not current user
FirebaseManager.shared.firestore
.collection("users")
.document(userUID) // << passed in from view
.collection("userRecipes")
.addSnapshotListener { (snapshot, err) in
guard let documents = snapshot?.documents else{
print("no documents present")
return
}
self.itemsNonUser = documents.map { (querySnapshot) -> RecipeItem in
let data = querySnapshot.data()
let recipeTitle = data ["recipeTitle"] as? String ?? ""
let recipePrepTime = data ["recipePrepTime"] as? String ?? ""
let recipeImage = data ["recipeImage"] as? String ?? ""
let createdAt = data ["createdAt"] as? String ?? ""
let ingredients = data ["ingredientItem"] as? [String: String] ?? ["": ""]
let directions = data ["directions"] as? [String] ?? [""]
let recipeID = data ["recipeID"] as? String ?? ""
let recipeCaloriesMacro = data ["recipeCaloriesMacro"] as? Int ?? 0
let recipeFatMacro = data ["recipeFatMacro"] as? Int ?? 0
let recipeCarbMacro = data ["recipeCarbMacro"] as? Int ?? 0
let recipeProteinMacro = data ["recipeProteinMacro"] as? Int ?? 0
let recipe = RecipeItem(id: recipeID, recipeTitle:recipeTitle , recipePrepTime: recipePrepTime, recipeImage: recipeImage, createdAt: createdAt, recipeCaloriesMacro: recipeCaloriesMacro, recipeFatMacro: recipeFatMacro, recipeCarbMacro:recipeCarbMacro, recipeProteinMacro: recipeProteinMacro, directions: directions, ingredientItem: ingredients)
return recipe
}
}
}
Try this example code to update the View when it first appears.
// for testing
struct RecipeItem {
var recipeTitle: String
// ...
}
class TestClass: ObservableObject {
#Published var itemsNonUser = [RecipeItem]() // <-- here
func grabItems(userUID: String){
//...
// for testing
itemsNonUser = [RecipeItem(recipeTitle: "banana cake with ID: \(userUID)")]
}
}
struct TestStruct: View {
#StateObject var rm = TestClass() // <-- here
#State var userUID: String
var body: some View {
VStack {
Text(rm.itemsNonUser.first?.recipeTitle ?? "no name")
Text("count: \(rm.itemsNonUser.count)") // <-- here
}
.onAppear{
rm.grabItems(userUID: userUID) // <-- here
}
}
}
struct ContentView: View {
let userUID = "123456"
var body: some View {
TestStruct(userUID: userUID) // <-- here
}
}
In my instance (and could have been specific for my particular case) I updated the grabItems recipe to return an Int since my view was trying to display the count.
I then just called it directly into the view like so and it worked:
Text(String(rm.grabItems(userUID: userUID))).bold() // << calling function directly in view

Swift, How to make user-input form data observable to a viewModel for use in viewModel's functions?

Using Swift programatically (no storyboard), I have a View ("ProfileFormView") which contains a 'Form {}' with multiple pickers and datePickers. I need my ViewModel ("EarningsViewModel") to observe and utilize the information the user selected in the ProfileFormView Form{}. How do I make the form's user-inputs observable to my viewModel?
View code:
struct ProfileFormView: View {
#State private var birthdate = Date()
#State private var newHireDate = Date()
#State private var chosenRetireAge = 65
let retirementAges = 60...65
#State private var chosenHours = 76
let hoursFlown = 68...95
#State private var chosenAircraft = "B717"
let currentAircraft = ["B717","B737","B757","B767-4","A220","A320","A330","A350"]
#State private var chosenSeat = "Captain"
let twoSeats = ["First Officer", "Captain"]
#State private var chosenYearUpgrade = 10
let yearToUpgrade = 2...30
#State private var chosenYearUpgrade2 = 10
let yearToUpgrade2 = 2...30
#State private var chosenAircraft2 = "A330"
let secondAircraft = ["B717","B737","B757","B767-4","A220","A320","A330","A350"]
#State var upgraded: Bool
#State var upgraded2: Bool
#State var planningToUpgrade: Bool
#State var planningToUpgrade2: Bool
#State var planningToSwitch: Bool
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal Information")) {...
ViewModel code:
class EarningsViewModel: ObservableObject {
func calcYearsRemaining(retireAge: Int, birthday: String) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
let birthdayDate = dateFormater.date(from: birthday)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthdayDate!, to: now, options: [])
let age = calcAge.year
let yearsRemaining = retireAge - age!
return yearsRemaining
....}
In order to pull data from a Model I make the Model an ObservableObject. Xcode tells me it's not permissible to make my View (FormProfileView) an ObservableObject. The user selects their birthdate, the date they starting working for the company, the age they plan to retire, etc... and I need to use that data in functions in my "EarningsViewModel"
Thank you for your help!
[image of form][1]
[1]: https://i.stack.imgur.com/nyPf9.png

Swift: Thread 1: signal SIGABRT- Getting and displaying data from Firestore

I'm a beginner trying to get the app to be able to post and see posts that are in Firebase. Every time I run this part of the app I get an error and the app crashes. Any help?
The error occurs on this line: let time = doc.document.data()["time"] as! Timestamp
(Thread 1: signal SIGABRT)
This is part of my code,
import SwiftUI
import Firebase
class PostViewModel : ObservableObject{
#Published var posts : [PostModel] = []
#Published var noPosts = false
#Published var newPost = false
#Published var updateId = ""
init() {
getAllPosts()
}
func getAllPosts(){
ref.collection("Posts").addSnapshotListener { (snap, err) in
guard let docs = snap else{
self.noPosts = true
return
}
if docs.documentChanges.isEmpty{
self.noPosts = true
return
}
docs.documentChanges.forEach { (doc) in
// Checking If Doc Added...
if doc.type == .added{
// Retrieving and appending...
let title = doc.document.data()["title"] as! String
let time = doc.document.data()["time"] as! Timestamp
let pic = doc.document.data()["url"] as! String
let userRef = doc.document.data()["ref"] as! DocumentReference

Firestore responding with "cannot find 'cards' in scope"

I followed this tutorial to get data from firestore and changed what i needed to correspond to my model but it keeps responding with "cannot find 'cards' in scope" and I'm not sure what i did wrong. (i think i got the mvvm labels right)
VIEW
import SwiftUI
struct TestingView: View {
#ObservedObject private var viewModel = CardViewModel()
var body: some View {
List(viewModel.cards) {
Text(cards.name)
}
.onAppear() {
self.viewModel.fetchData()
}
}
}
VIEW MODEL
import Foundation
import Firebase
class CardViewModel: ObservableObject {
#Published var cards = [Cards]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("cards").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.cards = documents.map { queryDocumentSnapshot -> Cards in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let pronoun = data["pronoun"] as? String ?? ""
let bio = data["bio"] as? String ?? ""
let profileURLString = data["profileURLString"] as? String ?? ""
let gradiantColor1 = data["gradiantColor1"] as? UInt ?? 0
let gradiantColor2 = data["gradiantColor2"] as? UInt ?? 0
let gradiantColor3 = data["gradiantColor3"] as? UInt ?? 0
return Cards(name: name, pronoun: pronoun, bio: bio, profileURLString: profileURLString, gradiantColor1: gradiantColor1, gradiantColor2: gradiantColor2, gradiantColor3: gradiantColor3)
}
}
}
}
MODEL
import Foundation
struct Cards: Identifiable {
var id = UUID().uuidString
var name: String
var pronoun: String
var bio: String
var profileURLString: String
var gradiantColor1: UInt
var gradiantColor2: UInt
var gradiantColor3: UInt
var profileURL: URL {
return URL(string: profileURLString)!
}
}
List will provide an element to its trailing closure -- see card in in my code. Then, you can access that specific card in your Text element.
var body: some View {
List(viewModel.cards) { card in //<-- Here
Text(card.name) //<-- Here
}
.onAppear() {
self.viewModel.fetchData()
}
}
}
I'd suggest that you might want to rename the struct Cards to struct Card since it is one card. Then, your array would be #Published var cards = [Card]() -- ie an array of Cards. From a naming perspective, this would make a lot more sense.

I cant present my view with foreach in SwiftUI

I try to present a View-struct as often as there are items in an array. I want to use for-each, because I don't like the UIList view. Btw I'm using SwiftUI. I generate the array which I want to use from firebase-firestore.
Here is how I generate my array:
class ViewModellForItems: ObservableObject{
#Published var listItemsEnglisch = [MaterialItemClass]()
let myDataBase = Firestore.firestore()
let Ordner = Firestore.firestore().collection("Censored")
func updateData(){
Ordner.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("Ordner wurde nicht gefunden")
return
}
self.listItemsMathe = documents.map { (queryDocumentSnapshot) -> MaterialItemClass in
let data = queryDocumentSnapshot.data()
let Name = data["Name"] as? String ?? ""
let beschreibung = data["beschreibung"] as? String ?? ""
let anzahlDesProduktes = data["anzahlDesProduktes"] as? Int ?? 0
let bildName = data["bildName"] as? String ?? ""
let hintergrundFarbe = data["hintergrundFarbe"] as? String ?? ""
let item = MaterialItemClass(Name: Name, beschreibung: beschreibung, anzahlDesProduktes: anzahlDesProduktes, bildName: bildName, hintergrundFarbe: hintergrundFarbe)
return item
}
}
}
}
Here is the Struct that I use in the ViewModellForItems Class:
struct MaterialItemClass {
var Name: String
var beschreibung: String
var anzahlDesProduktes: Int
var bildName: String
var hintergrundFarbe: String
}
And here is my ContendView.swift File:
struct ContendView: View {
#ObservedObject private var viewModel = ViewModellForItems()
var body: some View {
ForEach(0 ..< viewModel.listItemsEnglisch.count, id: \.self) {
Text(viewModel.listItemsEnglisch[$0].Name)
}.onAppear(){
self.viewModel.updateData()
}
Text("Debug")
}
}
I only get presented the Debug-Text... what am I doing wrong? And further; how can I present a whole View-Struct for each element I this array?
Just want to say, there's no fail of the firebase, because if I run almost the same code in a list view, everything is working fine...
Ok boys I got my mistake,
if I update my Array only in the .onAppear Methode of my for each block, it won't update, because the for-each block will never appear.
Thanks for your time
Boothosh