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
Related
I am trying to update my UI each time there have been changes in firestore document.
I when I check with console, I see that the listener fires each time I change document.
My listener and 'readyOrders' is #Published:
func getReadyOrders() {
referance
.collection(path)
.document(email)
.collection("CompletedOrders")
.whereField("placedBy", isEqualTo: user)
.addSnapshotListener { orderSnapshot, error in
DispatchQueue.main.async {
guard let snapshot = orderSnapshot?.documents else{
print("There is no active orders")
return
}
self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
let data = activeSnapshot.data()
var collectedItems = [MenuItem]()
var collectedDrinks = [DrinkItem]()
let id = activeSnapshot.documentID
let placed = data["placedBy"] as? String ?? ""
let inZone = data["inZone"] as? String ?? ""
let forTable = data["forTable"] as? String ?? ""
let orderItems = data["orderItems"] as? [String]
let orderDrinks = data["orderDrinks"] as? [String]
let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
let totalAmount = data["totalAmount"] as? Double ?? 0.00
orderItems?.forEach({ item in
let parts = item.components(separatedBy: "/")
collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
})
orderDrinks?.forEach({ drink in
let itemPart = drink.components(separatedBy: "/")
collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
})
return ActiveOrder(id: id,
placedBy: placed,
inZone: inZone,
forTable: forTable,
orderItems: collectedItems,
orderDrinks: collectedDrinks,
orderItemsReady: orderItemsReady,
orderDrinksReady: orderDrinksReady,
totalAmount: totalAmount)
}
}
}
}
View where I display all the documents
Note: This UI is updating when there is added new document or deleted current one.
Section {
ForEach(handler.readyOrders, id: \.id){ readyOrder in
NavigationLink{
OrderComplete(handler: handler, order: readyOrder, currency: currency)
} label: {
HStack{
Text(readyOrder.inZone!)
Text("- \(readyOrder.forTable!)")
}
}
}
} header: {
Text("Order's ready:")
}
And in this view I display the content of document, right in here the view does not update. To the file where I am displaying content I pass in the readyOrder from 'ForEach' and there I take the array in ready order and display it in 'ForEach':
ForEach(order.orderItems!, id:\.id){ item in
HStack{
Text(item.itemName!)
Spacer()
Text(currency.format(item.price!))
.foregroundColor(.teal)
Image(systemName: "arrow.left")
.foregroundColor(.red)
}
}
.onDelete(perform: deleteItem)
I have tried many things, and I am sure there is a simple solution, that I dont quite get. Because I am new to SwiftUI.
Edit:
I have puted together the the code for minimal repruduction as requested so there would more context for what I am trying to do.
Model:
struct Order: Identifiable{
var id = UUID().uuidString
var placedBy: String?
var inZone: String?
var forTable: String?
var orderItems: [MenuItem]?
var orderDrinks: [DrinkItem]?
var orderItemsReady: Bool?
var orderDrinksReady: Bool?
var totalAmount: Double?}
ViewModel:
class ViewModel: ObservableObject{
#Published var orders = [Order]()
private var referance = Firestore.firestore()
func getReadyOrders() {
referance
.collection(path)
.document(email)
.collection("CompletedOrders")
.whereField("placedBy", isEqualTo: user)
.addSnapshotListener { orderSnapshot, error in
DispatchQueue.main.async {
guard let snapshot = orderSnapshot?.documents else{
print("There is no active orders")
return
}
self.readyOrders = snapshot.map{ activeSnapshot -> ActiveOrder in
let data = activeSnapshot.data()
var collectedItems = [MenuItem]()
var collectedDrinks = [DrinkItem]()
let id = activeSnapshot.documentID
let placed = data["placedBy"] as? String ?? ""
let inZone = data["inZone"] as? String ?? ""
let forTable = data["forTable"] as? String ?? ""
let orderItems = data["orderItems"] as? [String]
let orderDrinks = data["orderDrinks"] as? [String]
let orderItemsReady = data["orderItemsReady"] as? Bool ?? false
let orderDrinksReady = data["orderDrinksReady"] as? Bool ?? false
let totalAmount = data["totalAmount"] as? Double ?? 0.00
orderItems?.forEach({ item in
let parts = item.components(separatedBy: "/")
collectedItems.append(MenuItem(itemName: parts[0], price: Double(parts[1])))
})
orderDrinks?.forEach({ drink in
let itemPart = drink.components(separatedBy: "/")
collectedDrinks.append(DrinkItem(drinkName: itemPart[0], price: Double(itemPart[1])))
})
return ActiveOrder(id: id,
placedBy: placed,
inZone: inZone,
forTable: forTable,
orderItems: collectedItems,
orderDrinks: collectedDrinks,
orderItemsReady: orderItemsReady,
orderDrinksReady: orderDrinksReady,
totalAmount: totalAmount)
}
}
}
}
func delteItem(menuItem: MenuItem, from order: ActiveOrder){
let item = menuItem.itemName! + "/" + String(menuItem.price!)
let pathTo = referance.collection(path).document(email).collection("CompletedOrders").document(order.id)
pathTo.getDocument { snapshot, error in
if let document = snapshot, document.exists{
var items = document.data()!["orderItems"] as? [String] ?? []
let index = items.firstIndex(where: { $0 == item })
items.remove(at: index!)
pathTo.updateData(["orderItems" : items]){ error in
if let _ = error{
print("Error deleting and updating order array")
}
}
}
}}}
And the veiws:
struct View1: View{
#ObservedObject var viewModel: ViewModel
var body: some View{
VStack{
ForEach(viewModel.orders, id: \.id){ readyOrder in
NavigationLink{
View2(viewModel: viewModel, order: readyOrder)
} label: {
HStack{
Text(readyOrder.inZone!)
Text("- \(readyOrder.forTable!)")
}
}
}
}
}}
struct View2: View{
#ObservedObject var viewModel: ViewModel
var order: Order
func deleteItem(at offest: IndexSet){
let index = offest[offest.startIndex]
let deleteItem = order.orderItems![index]
handler.delteItem(menuItem: deleteItem, from: order)
}
//In this view I want to get updated elements from document to display -> or if removed.
var body: some View{
VStack{
ForEach(order.orderItems!, id:\.id){ item in
HStack{
Text(item.itemName!)
Spacer()
Text(item.price!)
.foregroundColor(.teal)
Image(systemName: "arrow.left")
.foregroundColor(.red)
}
}
.onDelete(perform: deleteItem)
}
}}
SOLVED
I literally dont know why but when I changed my refarance from:
#ObservedObject var viewModel: Viewmodel
//To the stateobject
#StateObject var viewModel: ViewModel
As I understand the #StateObject in swift is used first time initializing view model class, and then you should use #ObservedObject as passing the view model further. If any body could explain me why in this case the state object worked it would be nice.
I need to fetch some data before passing it to a View. I created a ViewModel BoughtItemViewModel for the my BoughtItemView.
For simplicity I only provide one case, my enum has some more cases I switch through depending on what item is bought. This is the BoughtItemView:
enum SomeType {
case song(Song)
}
struct BoughtItemCard: View {
#State var type: SomeType
#StateObject var vm = BoughtItemViewModel()
var body: some View {
switch type {
case .song(let song):
WebImage(url: URL(string: song.image))
.resizable()
.frame(width: 150, height: 150)
.overlay(BoughtItemOverlay(type: type)
.environmentObject(vm)
.onAppear() {
vm.fetchUnlockDate(path: "songs", docId: song.id ?? "")
}
}
}
This is the BoughtItemViewModel:
class BoughtItemViewModel: ObservableObject {
#Published var unlockDate = Date()
func fetchUnlockDate(path: String, docId: String) {
let uid = FirebaseManager.shared.auth.currentUser?.uid ?? ""
FirebaseManager.shared.firestore.collection(path)
.document(docId).collection("unlocked").document(uid).getDocument { docSnapshot, error in
if let error = error {
print(error)
return
}
let data = docSnapshot?.data()
self.unlockDate = (data?["unlockDate"] as? Date ?? Date())
}
}
}
Now I want the unlockDatein my BoughtItemOverlay View to show the fetched date.
Again for simplicity I provide one case, this is the BoughtItemOverlay:
struct BoughtItemOverlay: View {
#State var showModal: Bool = false
#State var type: SomeType
#State var unlockDate = Date()
#EnvironmentObject var vm: BoughtItemViewModel
var body: some View {
switch type {
case .song(let song):
VStack {
Spacer().onAppear() {
unlockDate = vm.unlockDate
}
Text("Purchased \(unlockDate.formatted(.dateTime.day().month(.defaultDigits).year()))")
}
}
}
}
Instead of displaying the unlockDate it always displays the date of today. I'm not sure why that is since the BoughtItemOverlayshould refresh after the State changes in the onAppear() setting the value to the BoughtItemViewModel value. Atleast that I think it should work that way, but correct me if I'm wrong.
With #State var unlockDate = Date() you are creating a new source of truth and try to synchronize it with unlockDate = vm.unlockDate.
DonĀ“t do that, use the Viewmodel itself in your Childview:
struct BoughtItemOverlay: View {
#State var showModal: Bool = false
#State var type: SomeType
#EnvironmentObject var vm: BoughtItemViewModel
var body: some View {
switch type {
case .song(let song):
VStack {
Spacer()
// use the vm value here
Text("Purchased \(vm.unlockDate.formatted(.dateTime.day().month(.defaultDigits).year()))")
}
}
}
}
The Viewmodel will notify the View when unlockDate has changed and the View will show the new date.
Firebase returns a FIRTimestamp so I had to change my code to:
import Foundation
import FirebaseFirestore
class BoughtItemViewModel: ObservableObject {
#Published var unlockDate = Date()
func fetchUnlockDate(path: String, docId: String) {
let uid = FirebaseManager.shared.auth.currentUser?.uid ?? ""
FirebaseManager.shared.firestore.collection(path).document(docId).collection("unlocked").document(uid).getDocument { docSnapshot, error in
if let error = error {
print(error)
return
}
let data = docSnapshot?.data()
let timestamp = data?["unlockDate"] as? Timestamp
self.unlockDate = timestamp?.dateValue() ?? Date()
}
}
}
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.
#here is my view model#
import Foundation
class CategoriesModel:ObservableObject {
#Published var restaurantList = [Categories]()
init() {
//get path to json data
let pathString = Bundle.main.path(forResource: "data", ofType: "json")
if let path = pathString {
// create url object
let url = URL(fileURLWithPath: path)
do {
//create data object with data at url
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
do {
let restaurantData = try decoder.decode([Categories].self, from: data)
self.restaurantList = restaurantData
//restaurantList.shuffle()
}
catch {
//error decoding json
print(error)
}
}
catch {
//error fetching url
print(error)
}
}
}
}
#here is my model#
import Foundation
struct Categories: Identifiable, Decodable {
var id = 0
var name:String = ""
var truckOrRestaurant = ""
var type:[String] = [String]()
var pic:String = ""
var price:String = ""
var hours:[String:[String]] = [String:[String]]()
var hoursString:String = ""
var stars:String = ""
var website:String = ""
var location:String = ""
var coordinates:[Double] = [Double]()
var phoneNumber:String = ""
var currency:String = ""
var review:String = ""
var wait:String = ""
}
#some of one of my views#
import SwiftUI
import MapKit
struct DiscoverTab: View {
#EnvironmentObject var restaurantInfo:CategoriesModel
#StateObject private var location = LocationCode()
var body: some View {
VStack {
HStack {
Text("Sort by:")
Button(action:{
//sort alphabetically
//restaurantInfo.restaurantList.sort()
}, label:{
Text("Alphabetical")
.background(Color.yellow)
.foregroundColor(.black)
.cornerRadius(5)
})
Button(action:{
//sort alphabetically
//restaurantInfo.restaurantList.sort()
}, label:{
Text("Distance")
.background(Color.yellow)
.foregroundColor(.black)
.cornerRadius(5)
})
...
I believe I have to use some sort of different properties in the model, but how do I sort alphabetically, (.sort() doesn't work), and by different pieces of information found in my json. Also, how would I sort data that doesn't come from my json? I have a different function in my viewmodel that returns "open" if a restaurant is open at any given time or "closed" vice versa. Whenever I try to create a new published list in my viewmodel and append the restaurant name to it, I get an error. Is there some extra protocol I need to use or something?
The recommended way is to sort the data in the view model.
First of all name the model in singular form Category, one instance is one category
struct Category: Identifiable, Decodable {
in CategoriesModel add a method to sort the array by key paths
func sort<T : Comparable>(keyPath: KeyPath<Category,T>) {
restaurantList.sort { c1, c2 in
c1[keyPath: keyPath] < c2[keyPath: keyPath]
}
}
Then, in the button action call the custom sort method
Button(action:{
//sort alphabetically
restaurantInfo.restaurantList.sort(keyPath: \.name)
}
For the distance you have to add the logic also in the view model
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