LAST CHANCE: Save 50% on all my Swift books and bundles! >>

SOLVED: Day 37 – Project 7, part two : 2 problems (Even with the final code provided)

Forums > 100 Days of SwiftUI

I thought I had made a mistake, but I am having the same 2 problems with the code provided by Paul.

1) When you have finished entering an expense in the addVew and the sheet closes, you cannot reopen it to enter another expense. 2) Expenses are not saved (in userDefaults).

I run Xcode 11.4 Maybe this project need to be updated 🤷🏻‍♂️

I continue anyway!

[edit]

1) I noticed something strange, if we start slightly (without going all the way) delete the expense that we just inserted in the list, the add button becomes accessible again... 🤨

1) If you use a button outside of the navigationBarItems (after the list for example), there is no more problem.

3      

Hi @linkiliz,

1) When you have finished entering an expense in the addVew and the sheet closes, you cannot reopen it to enter another expense.

I have also experienced this issue and its a bug as mentioned here and a work around given there. Also I've noticed that if you wait for a while after dismissing the sheet and that button starts working again!! Weird but true!

2) Expenses are not saved (in userDefaults).

Mine is working fine. So can you show the piece of code you have tried?

3      

Thank you @iRiziya
1) thank you for this.

2) Even on the code given by Paul (https://github.com/twostraws/HackingWithSwift).

Whatever, this is the code :

import SwiftUI

struct ExpenseItem: Identifiable, Codable {
    let id = UUID()
    let name: String
    let type: String
    let amount: Int
}

class Expenses: ObservableObject {
    @Published var items: [ExpenseItem] {
        didSet {
            let encoder = JSONEncoder()
            if let encoded = try? encoder.encode(items) {
                UserDefaults.standard.set(encoded, forKey: "Items")
                print("data encoded")
            }
        }
    }

    init() {
        if let items = UserDefaults.standard.data(forKey: "Items") {
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode([ExpenseItem].self, from: items) {
                self.items = decoded
                print("data decoded")
                return
            }
        }

        self.items = []
    }
}

struct ContentView: View {
    @ObservedObject var expenses = Expenses()
    @State private var showingAddExpense = false

    var body: some View {
        NavigationView {
            List {
                ForEach(expenses.items) { item in
                    HStack {
                        VStack(alignment: .leading) {
                            Text(item.name)
                                .font(.headline)
                            Text(item.type)
                        }

                        Spacer()
                        Text("$\(item.amount)")
                    }
                }
                .onDelete(perform: removeItems)
            }
            .navigationBarTitle("iExpense")
            .navigationBarItems(trailing:
                Button(action: {
                    self.showingAddExpense = true
                }) {
                    Image(systemName: "plus")
                }
            )
            .sheet(isPresented: $showingAddExpense) {
                AddView(expenses: self.expenses)
            }
        }
    }

    func removeItems(at offsets: IndexSet) {
        expenses.items.remove(atOffsets: offsets)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I added print when data is supposed to be archived/unarchived, nothing happens.

3      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

I have tried you code and without changing a single line of code, its working for me!

I mean it saves data in UserDefaults and displaying properly as well.

3      

You mean even if you close the app, then restart, data is always here ? (i guess yes but just to be sure)

Weird...🧐I don't understand why it doesn't work for me.

What is your Xcode version ?

3      

yes! I am using Xcode 11.3

3      

Thank you @iRiziya. So I think it's a version problem, I run 11.4 I will come back to test after the Xcode updates, the main thing is that I understood how it works.

3      

Ok fine. Glad to know that. Good luck.

3      

There is apparently a bug in Swift 5.2 with respect to didSet when called on a @Published property wrapper. didSet will no longer fire when the property changes. I guess I should say that I think it is a bug. It could be an intentional change for all I know. But I was banging my head against the same problem for a long time. But the bottom line is that Project 7 as written no longer properly saves data because didSet no longer fires.

3      

Agree @GrokTime. Just noticed that when I ran the project a few moments ago. I finished the course in early January so it's shame to see that now not working as is.

The solution for me was to add a saveToUserDefaults() function in AddView and place the code that saves to UserDefaults there and call that function from the save button.

    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $name)
                Picker("Type", selection: $type) {
                    ForEach(Self.types, id: \.self) {
                        Text($0)
                    }
                }
                TextField("Amount", text: $amount)
                    .keyboardType(.decimalPad)
            }
            .navigationBarTitle("Add new expense")
            .navigationBarItems(leading: Button("Cancel") {
                self.presentationMode.wrappedValue.dismiss()
                },
                trailing: Button("Save") {
                if let actualAmount = Float(self.amount) {
                    let item = ExpenseItem(name: self.name == "" ? "Miscellaneous" : self.name, type: self.type, amount: actualAmount)
                    self.expenses.items.append(item)

                    self.saveToUserDefaults()

                    self.presentationMode.wrappedValue.dismiss()
                } else {
                    self.showingAlert = true
                }
            })
        }
        .alert(isPresented: $showingAlert) {
            Alert(title: Text("Error"), message: Text("Amount must be numeric"), dismissButton: .default(Text("Continue")))
        }
    }

    func saveToUserDefaults() {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(expenses.items) {
            UserDefaults.standard.set(encoded, forKey: "Items")
        }
    }

Well, it does not end there. Same thing required in the removeItems() function. What a pain.

3      

I ended up coming back to this one again. The other solution, though ugly, is to assign the existing array to a temporay array and then removeAll() elements from the original array and then assign the temporary array back to the orignal.

let tempItems = self.expenses.items
self.expenses.items.removeAll()
self.expenses.items = tempItems

It's a cludge but works.

3      

@ChrisParkerWA

Ugly but faster! 👍 I really wonder how such a functionality can be suddenly unusable ... 😳

3      

A Bug report is in progress with Swift.org

Details here: https://bugs.swift.org/browse/SR-12089

3      

Thanks! After trying half an hour why my code was not working, I decided to check the forums. Good to know what's the cause of this problem (I am also using Xcode 11.4).

3      

I ran into this problem today and came up with the following solution... You can add the following modifier to any view in ContentView. I added it to the List after .sheet

.onReceive(expenses.$items) { items in
    // This is used as a workaround to write to UserDefaults every time the items property of the expenses class is updated.
    // This is necessary because the didSet property observer currently does NOT fire on a property with the @Published property wrapper.
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(items) {
        UserDefaults.standard.set(encoded, forKey: "Items")
    }
}

4      

OMG... I've spend a whole afternoon trying to figure that out before finding this post... Thank you guys! 😁

3      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.