Firebase issue updating data when it changes

Hey all,

I’m really hoping someone has some thoughts here. I’ve spent a week trying to solve this with Firebase documentation, google, CWC lessons, youtube videos and I’m just stuck. Essentially I’m creating a specialized todo list app. The view model retrieves the data, parses it and puts it into a published array and displays it. That works fine. You can then pull up the detail for any list (this screen isn’t complete yet but that’s irrelevant to the problem), or create a new list. Here’s where the problem comes. If I modify or delete an existing list, it seems as though the published property isn’t alerting the environment object and redrawing the top level view that shows the list of budgets. There are three pieces of data that should update for every list on that screen. The Name, the Icon and the color. The color actually appears to be properly updating but nothing else is. In addition, when you update an existing list, the detail for the list doesn’t update either.

IF you exit the app and restart it, then the data is properly displayed. I’ll paste code from the model, view model, and three views below. If someone wants to dig in further and view the actual project, send me your email and I’ll send you a link the the private github repo.

MODEL

import Foundation

struct Budget: Identifiable {
    
    var id: String = ""
    var ownerId: String = ""
    var sharedWith = [String]()
    var name: String = ""
    var icon: String = ""
    var iconColorName: String = "blue"
    var budget: Double = 0.0
    var listItems = [Item]()
    var cartItems = [Item]()
    
}

VIEW MODEL

import SwiftUI
import Firebase
import FirebaseAuth

class BudgetModel: ObservableObject, Identifiable {
    
    // Budget list
    @Published var budgets = [Budget]()
    
    // Firebase database reference
    let db = Firestore.firestore()
    
    init() {
        getBudgetData()
    }
    
    // MARK: UI Methods
    func iconColor(iconColorName: String) -> Color {
        if iconColorName == "blue" {
            return Constants.white
        } else if iconColorName == "red" {
            return .red
        } else if iconColorName == "orange" {
            return .orange
        } else if iconColorName == "yellow" {
            return .yellow
        } else if iconColorName == "green" {
            return .green
        } else if iconColorName == "purple" {
            return .purple
        } else {
            return Constants.white
        }
    }
    
    // MARK: Firebase methods
    func getBudgetData() {

        var userBudgets = [Budget]()
        
        // Make sure user is logged in
        guard let currentUser = Auth.auth().currentUser else {
            print("❌ No user logged in")
            return
        }
        
        // Get a reference to all documents owned by the current user
        let ref = db.collection("budgets").whereField("ownerId", isEqualTo: currentUser.uid)
        
        ref.addSnapshotListener { [weak self] querySnapshot, error in
            if let error = error {
                print("❌ Failed to retrieve budgets with users ID: \(error)")
            }
            
            guard let snapshot = querySnapshot else {
                print("❌ Error fetching snapshots: \(error!)")
                return
            }
            
            snapshot.documentChanges.forEach { diff in
                
                if (diff.type == .added) {
                    let newBudget = self?.parseBudget(diff.document)
                    if let newBudget = newBudget {
                        userBudgets.append(newBudget)
                    }
                    self?.budgets = userBudgets
                }
                
                if (diff.type == .modified) {
                    
                    for (i, budget) in self!.budgets.enumerated() {
                        if budget.id == diff.document.documentID {
                            self?.budgets[i] = self!.parseBudget(diff.document)
                            break
                        }
                    }
                    
                    print("🖕 Modified budget: \(diff.document.data())")
                    print("with IDt: \(diff.document.documentID)")
                }
                
                if (diff.type == .removed) {
                    
                    for (i, budget) in self!.budgets.enumerated() {
                        if budget.id == diff.document.documentID {
                            print(self?.budgets.count)
                            self?.budgets.remove(at: i)
                            print(self?.budgets.count)
                            break
                        }
                    }
                    
                    print("🖕 Removed budget: \(diff.document.data())")
                }
            }
        }
    }
    
    func updateBudget(budgetId: String?, name: String, icon: String, iconColorName: String, listBudget: Double) {
        
        guard let currentUser = Auth.auth().currentUser else {
            print("❌ No user logged in")
            return
        }
        
        let ownerId = currentUser.uid
        
        // If budgetId is not nil access that document
        let doc: DocumentReference
        if let budgetId = budgetId {
            doc = db.collection("budgets").document(budgetId)
        } else {
            doc = db.collection("budgets").document()
        }
        
        doc.setData(["ownerId":ownerId, "name":name, "icon":icon, "iconColorName":iconColorName, "budget":listBudget], merge: true) { error in
            if let error = error {
                print("❌ Failed to update budget: \(error)")
            }
        }
        
        db.collection("users").document(currentUser.uid).collection("budgets").document(doc.documentID).setData(["date":Date()]) { error in
            if let error = error {
                print("❌ Could not add budget to user: \(error)")
            }
            
            self.getBudgetData()
        }
        
    }
    
    func deleteBudget(_ budgetId: String) {
        
        guard let currentUser = Auth.auth().currentUser else {
            print("❌ No user logged in")
            return
        }
        
        if budgetId == "" { return }
        
        let doc = db.collection("budgets").document(budgetId)
        doc.delete() { error in
           
            if let error = error {
                print("❌ Could not delete budget: \(error)")
            }
            
        }
        
    }
    
    func parseBudget(_ doc: QueryDocumentSnapshot) -> Budget {
        var newBudget = Budget()
        
        newBudget.id = doc.documentID
        newBudget.name = doc["name"] as? String ?? ""
        newBudget.ownerId = doc["ownerId"] as? String ?? ""
        // TODO: Implement sharing
        newBudget.sharedWith = [String]()
        newBudget.icon = doc["icon"] as? String ?? "list.bullet.circle"
        newBudget.iconColorName = doc["iconColorName"] as? String ?? "blue"
        newBudget.budget = doc["budget"] as? Double ?? 0.0
        newBudget.listItems = [Item]()
        newBudget.cartItems = [Item]()
        
        return newBudget
    }
    
}

BUDGET LIST VIEW

import SwiftUI

struct Budgets: View {
    
    // Data Models
    @EnvironmentObject var budgetModel: BudgetModel
    
    // State properites
    @State var showingBudgetDetail = false
    
    // Other Properties
    var layout = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        NavigationView {
            ZStack {
                Constants.bgGradient
                    .ignoresSafeArea()
                
                if budgetModel.budgets.count > 0 {
                    
                    // MARK: Budgets exist
                    ScrollView(showsIndicators: false) {
                        LazyVGrid(columns: layout, spacing: 20) {
                            
                            // Cycle through budgets array and create an icon for each
                            ForEach(budgetModel.budgets) { budget in
                                NavigationLink {
                                    // Open budget detail for selected list and send index
                                    BudgetDetail(budget: budget)
                                } label: {
                                    BudgetListButton(
                                        icon: budget.icon,
                                        iconColorName: budget.iconColorName,
                                        name: budget.name
                                    )
                                }
                            }
                            .padding(.bottom, 20)
                            
                            // Add new budgets
                            Button {
                                showingBudgetDetail = true
                            } label: {
                                BudgetListButton(icon: "plus", iconColorName: "blue", name: "New Budget")
                            }
                            .sheet(isPresented: $showingBudgetDetail) { } content: {
                                BudgetSetup()
//                                BudgetSetup(showingBudgetDetail: $showingBudgetDetail)
                            }
                            .padding(.bottom, 30)
                            
                        }
                        .padding(.horizontal)
                        
                    }
                    .navigationBarTitle("Budgets")
                    .navigationBarTitleDisplayMode(.inline)
                    .padding(.top, 30)
                    
                } else {
                    
                    // MARK: Budgets do not exist
                    VStack {
                        
                        Button {
                            showingBudgetDetail = true
                        } label: {
                            BudgetListButton(icon: "plus", iconColorName: "blue", name: "Oh no, this looks empty!\nLet's add your first list.", width: 120, height: 120)
                        }
                        .sheet(isPresented: $showingBudgetDetail) { } content: {
                            BudgetSetup()
                        }
                        
                    }
                }
            }
        }
    }
    
}

BUDGET DETAIL VIEW

import SwiftUI

struct BudgetDetail: View {
    
    // Environment
    @EnvironmentObject var budgetModel: BudgetModel
    @Environment(\.dismiss) var dismiss
    
    // State Properties
    @State var budgetListTab = Constants.BudgetDetailTab.list
    @State var showingBudgetDetail = false
    @State var budget: Budget
    
    var body: some View {
        
        VStack {
            Picker(selection: $budgetListTab) {
                Text(budget.name)
                    .tag(Constants.BudgetDetailTab.list)
                Text("Cart")
                    .tag(Constants.BudgetDetailTab.cart)
            } label: {
                Text("Budget Detail Tab")
            }
            .pickerStyle(.segmented)
            .padding(.horizontal)
            
            if budgetListTab == .list {
                BudgetDetailList(budget: budget)
            } else if budgetListTab == .cart {
                BudgetDetailCart(budget: budget)
            }
            Spacer()
        }
        .sheet(isPresented: $showingBudgetDetail) { } content: {
            BudgetSetup(budget: budget)
        }
        .toolbar {
            Button("Edit") {
                showingBudgetDetail = true
            }
        }
        
    }
}

BUDGET SETUP/MODIFY SHEET VIEW

import SwiftUI

struct BudgetSetup: View {
    
    // Environment
    @EnvironmentObject var budgetModel: BudgetModel
    @Environment(\.presentationMode) var presentationMode
    
    // Budget
    @State var budget: Budget?
    
    // State Properties
    @State var iconColor: Color?
    @State var iconColorName: String = "blue"
    @State var nameText: String = ""
    @State var budgetValue: Double?
    @State var icon: String = "list.bullet"
    
    @State var showingDeleteAlert = false
    @FocusState var valueIsFocused: Bool
    
    // Layout
    let iconData = Bundle.main.decode([String].self, from: "symbols.json")
    let layout = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        
        VStack {
            // MARK: Title Bar
            ZStack(alignment: .top) {
                HStack {
                    Button {
                        presentationMode.wrappedValue.dismiss()
                    } label: {
                        Text("Cancel")
                    }
                    Spacer()
                    Button {
                        self.budgetModel.updateBudget(
                            budgetId: budget?.id,
                            name: nameText == "" ? "Budget" : nameText,
                            icon: icon,
                            iconColorName: iconColorName,
                            listBudget: budgetValue ?? 0.0)
                        presentationMode.wrappedValue.dismiss()
                    } label: {
                        Text("Done")
                    }
                    
                }
                Text(budget?.name ?? "New Budget")
                    .bold()
                    .font(.title2)
            }
            .padding()
            
            // MARK: Icon, Name and Value
            Section {
                VStack {
                    HStack {
                        Spacer()
                        ZStack {
                            Circle()
                                .foregroundColor(iconColor)
                                .frame(width: 120, height: 120)
                                .shadow(radius: 5)
                            Image(systemName: icon)
                                .resizable()
                                .scaledToFit()
                                .foregroundColor(Constants.white)
                                .frame(width: 60, height: 60)
                        }
                        Spacer()
                    }
                    .padding(.vertical, 40)
                    TextField("Budget Name", text: $nameText)
                        .textFieldStyle(.roundedBorder)
                        .font(.title2)
                        .padding(.bottom, 10)
                    CurrencyField("Amount to Budget", value: Binding(get: {
                        budgetValue
                    }, set: { number in
                        budgetValue = number
                    }))
                    .focused($valueIsFocused)
                    .padding(.bottom, 20)
                }
            }
            .padding(.horizontal)
            
            // MARK: Color Selector
            Section {
                LazyVGrid(columns: layout, alignment: .center, spacing: 20, content: {
                    
                    Button {
                        iconColor = .red
                        iconColorName = "red"
                    } label: {
                        Circle()
                            .foregroundColor(.red)
                            .frame(width: 40, height: 40)
                    }
                    
                    Button {
                        iconColor = .orange
                        iconColorName = "orange"
                    } label: {
                        Circle()
                            .foregroundColor(.orange)
                            .frame(width: 40, height: 40)
                    }
                    
                    Button {
                        iconColor = .yellow
                        iconColorName = "yellow"
                    } label: {
                        Circle()
                            .foregroundColor(.yellow)
                            .frame(width: 40, height: 40)
                    }
                    
                    Button {
                        iconColor = .green
                        iconColorName = "green"
                    } label: {
                        Circle()
                            .foregroundColor(.green)
                            .frame(width: 40, height: 40)
                    }
                    
                    Button {
                        iconColor = Constants.blue
                        iconColorName = "blue"
                    } label: {
                        Circle()
                            .foregroundColor(.blue)
                            .frame(width: 40, height: 40)
                    }
                    
                    Button {
                        iconColor = .purple
                        iconColorName = "purple"
                    } label: {
                        Circle()
                            .foregroundColor(.purple)
                            .frame(width: 40, height: 40)
                    }
                    
                })
            }
            .padding(.horizontal)
            
            Divider()
                .padding(.top, 20)
            
            // MARK: Icon Selector
            ScrollView(showsIndicators: false) {
                Section {
                    LazyVGrid(columns: layout, alignment: .center, spacing: 20, content: {
                        ForEach(iconData, id: \.self) { icon in
                            Button {
                                self.icon = icon
                            } label: {
                                ZStack {
                                    Circle()
                                        .foregroundColor(Constants.lightGray)
                                        .frame(width: 40, height: 40)
                                    Image(systemName: icon)
                                        .foregroundColor(Constants.darkGray)
                                        .frame(width: 20, height: 20)
                                }
                            }
                        }
                    })
                    .padding(.vertical)
                }
                
            }
            .padding(.horizontal)
            
            // MARK: Delete Button
            if budget != nil {
                Button(action: {
                    showingDeleteAlert = true
                }, label: {
                    Text("Delete Budget")
                })
                .buttonStyle(BudgeButton(buttonColor: .red, textColor: Constants.white))
                .alert(isPresented: $showingDeleteAlert) {
                    Alert(
                        title: Text("Delete \(budget?.name ?? "Budget")"),
                        message: Text("Please tap Delete to confirm you would like to delete this budget."),
                        primaryButton: .default(
                            Text("Cancel")
                        ),
                        secondaryButton: .destructive(
                            Text("DELETE")) {
                                budgetModel.deleteBudget(budget?.id ?? "")
                                presentationMode.wrappedValue.dismiss()
                            }
                    )
                }
                .padding()
            }
            
        }
        .onTapGesture {
            dismissKeyboard()
        }
        .onAppear() {
            self.nameText = budget?.name ?? ""
            self.icon = budget?.icon ?? "list.bullet"
            self.iconColorName = budget?.iconColorName ?? "blue"
            let iconColor = budgetModel.iconColor(iconColorName: self.iconColorName)
            if iconColor == .white {
                self.iconColor = Constants.blue
            } else {
                self.iconColor = iconColor
            }
            self.budgetValue = budget?.budget
            print("TCB: Icon Color onAppear \(String(describing: self.iconColor))")
        }
    }
}

Anyone that may be able to help I owe you! Thanks for taking a look.

I finally figured this out!!! If anyone else is having this same problem, make sure that you don’t have a @State property wrapper on the properties that aren’t updating.

2 Likes