I want to record long term, how many times a specific ItemView has been displayed in my TabView below. Each time a user swipes on the tab, I want to update var timesViewed by 1. However, timesViewed doesn't seem to update and I am really stuck as to why now.
I removed some view modifiers to simplify the code below.
struct Item: Identifiable, Hashable, Codable {
var id = UUID()
var title: String
var detail: String
var repeatOn: String
var timesViewed = 0
mutating func viewedThisItem() {
timesViewed += 1
}
}
struct ItemSessionView: View {
var itemViewModel: ItemListVM
#State var count = 0
#State var currentIndex = 0
var body: some View {
let today = getTodaysDate().uppercased()
var tempList = itemViewModel.list.filter({ return $0.repeatOn == today})
ZStack {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
ForEach(tempList) { item in
Group {
if today == item.repeatOn {
ItemDetailView(item: item)
}
}
}
}
.onChange(of: currentIndex) { value in
tempList[currentIndex].viewedThisItem()
}
}
}
}
func getTodaysDate() -> String {
let today = Date.now
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
formatter.dateFormat = "E"
let todaysDate = formatter.string(from: today)
return todaysDate
}
}
Structs are value type, you modify the (copied) item in the filtered array but not the original item in the itemViewModel object.
A possible solution is to get the item in the itemViewModel object by id and modify that directly.
.onChange(of: currentIndex) { value in
let id = tempList[value].id
let index = itemViewModel.list.firstIndex{$0.id == id}!
itemViewModel.list[index].viewedThisItem()
}
Force unwrapping is safe because the item does exist.
Related
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
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'm using the DecimalField struct to place text fields in my app. However, if I use it alongside an environment object, the app freezes with a memory leak.
This is my model:
class PaymentPlan: ObservableObject {
#Published var amountBorrowed: Decimal?
}
This is my content view:
var currencyFormatter: NumberFormatter {
let nf = NumberFormatter()
nf.numberStyle = .currency
nf.isLenient = true
return nf
}
struct ContentView: View {
#EnvironmentObject var paymentPlan: PaymentPlan
static var currencyFormatter: NumberFormatter {
let nf = NumberFormatter()
nf.numberStyle = .currency
nf.isLenient = true
return nf
}
var body: some View {
DecimalField("Placeholder", value: $paymentPlan.amountBorrowed, formatter: Self.currencyFormatter)
}
}
This is the custom text field I am using (source):
import SwiftUI
import Combine
struct DecimalField : View {
let label: LocalizedStringKey
#Binding var value: Decimal?
let formatter: NumberFormatter
let onEditingChanged: (Bool) -> Void
let onCommit: () -> Void
// The text shown by the wrapped TextField. This is also the "source of
// truth" for the `value`.
#State private var textValue: String = ""
// When the view loads, `textValue` is not synced with `value`.
// This flag ensures we don't try to get a `value` out of `textValue`
// before the view is fully initialized.
#State private var hasInitialTextValue = false
init(
_ label: LocalizedStringKey,
value: Binding<Decimal?>,
formatter: NumberFormatter,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = {}
) {
self.label = label
_value = value
self.formatter = formatter
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
var body: some View {
TextField(label, text: $textValue, onEditingChanged: { isInFocus in
// When the field is in focus we replace the field's contents
// with a plain unformatted number. When not in focus, the field
// is treated as a label and shows the formatted value.
if isInFocus {
self.textValue = self.value?.description ?? ""
} else {
let f = self.formatter
let newValue = f.number(from: self.textValue)?.decimalValue
self.textValue = f.string(for: newValue) ?? ""
}
self.onEditingChanged(isInFocus)
}, onCommit: {
self.onCommit()
})
.onReceive(Just(textValue)) {
guard self.hasInitialTextValue else {
// We don't have a usable `textValue` yet -- bail out.
return
}
// This is the only place we update `value`.
self.value = self.formatter.number(from: $0)?.decimalValue
}
.onAppear(){ // Otherwise textfield is empty when view appears
self.hasInitialTextValue = true
// Any `textValue` from this point on is considered valid and
// should be synced with `value`.
if let value = self.value {
// Synchronize `textValue` with `value`; can't be done earlier
self.textValue = self.formatter.string(from: NSDecimalNumber(decimal: value)) ?? ""
}
}
.keyboardType(.decimalPad)
}
}
Any suggestions on what may not be working well? The text field works perfectly with #State.
Here is fixed part - to avoid cycling it needs to update only with really new value
Tested with Xcode 12 / iOS 14
.onReceive(Just(textValue)) {
guard self.hasInitialTextValue else {
// We don't have a usable `textValue` yet -- bail out.
return
}
// This is the only place we update `value`.
let newValue = self.formatter.number(from: $0)?.decimalValue
if newValue != self.value {
self.value = newValue
}
}
I store a value called month hours in my application that keeps track of the hours a person has used the apps and displays it in a line of text. The text if part of a stack in Swift UI, but I can't figure out how to make the text update once the information has been queried from I've tried quite a few ways of making this work from structs to classes to using #State.
This is just the latest thing I tried that didn't work if anyone can help that would be greatly appreciated.
let db = Firestore.firestore()
class Month {
var monthHours = "0"
func getMonthHours() {
db.addSnapshotListener(. //Im removing the actual query part to keep that private but the print statement below confirms the query is not the issue.
{ (docSnapShot, err) in
if let e = err {
print("There was an error retrieving the monthly hours:\n\(e.localizedDescription)")
} else {
let data = docSnapShot?.data()
if let h = data?[K.FStore.monthHoursField] as? Double {
self.monthHours = String(h.rounded())
print("These are the hours:\n\(self.monthHours)")
}
}
})
}
func getMonth() -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let result = formatter.string(from: date)
return result
}
init() {
getMonthHours()
}
}
struct ChartView : View {
#State private var month = Month()
//Struct variables
var body : some View {
ZStack {
Color(UIColor(named: K.BrandColors.grey)!).edgesIgnoringSafeArea(.all)
VStack {
Text("HOURS THIS MONTH \(month.monthHours)")
.font(.system(size: 18))
.fontWeight(.heavy)
}
}
}
This outlines one possible approach. The crux is to deal with the asynchronous function "getMonthHours". You need to wait till it is finished its fetching before you can use the results.
class Month {
var monthHours = "0"
// async fetch the month hours from Firestore, ... error handling todo
static func getMonthHours(handler: #escaping (String) -> Void) {
db.addSnapshotListener{ (docSnapShot, err) in
if let e = err {
print("There was an error retrieving the monthly hours:\n\(e.localizedDescription)")
return handler("") // should return some error here .... todo
} else {
if let data = docSnapShot?.data(),
let h = data?[K.FStore.monthHoursField] as? Double {
// return the result
return handler(String(h.rounded()))
} else {
return handler("") // should return some error here .... todo
}
}
}
}
func getMonth() -> String {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let result = formatter.string(from: date)
return result
}
init() { }
}
struct ChartView : View {
#State private var monthHours = ""
var body : some View {
ZStack {
Color(UIColor(named: K.BrandColors.grey)!).edgesIgnoringSafeArea(.all)
VStack {
Text("HOURS THIS MONTH \(monthHours)")
.font(.system(size: 18))
.fontWeight(.heavy)
}
}.onAppear(perform: loadData)
}
}
func loadData() {
// when the fetching is done it will update the view
Month.getMonthHours() { hours in
self.monthHours = hours
}
}