UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 38 Challenge - Couldn't get the EditButton() to work in the project

Forums > 100 Days of SwiftUI

I tried a variety of ways to get the EditButton to be added to the Navigation Bar and it would never render correctly (adding it as the leading by itself, trailing by itself, added as leading with leading & trailing properties, and added as trailing with leading & trailing properties). It is shown, but when you tap it the UI just flitters and the button title changes to "Done" but you never see the delete circles to the left of the rows. Is anyone else seeing this?

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

    var textColor: Color {
        var _textColor: Color

        if amount < 10 {
            _textColor = .black
        } else if amount < 100 {
            _textColor = .blue
        } else {
            _textColor = .green
        }

        return _textColor
    }
}

class Expenses: ObservableObject {
    @Published var items = [ExpenseItem]() {
        didSet {
            let encoder = JSONEncoder()

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

    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

                return
            }
        }

        self.items = []
    }
}

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

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

                            Spacer()
                            Text("$\(item.amount)")
                        }
                    }
                    .onDelete(perform: removeItems)
                }
                .navigationBarTitle("iExpense")
                .navigationBarItems(leading: EditButton())
                .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 AddView: View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var expenses: Expenses
    @State private var name = ""
    @State private var type = "Personal"
    @State private var amount = ""
    static let types = ["Business", "Personal"]

    @State private var showingInvalidAmountAlert = false

    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, onCommit:  {
                    showingInvalidAmountAlert = !amountInputIsValid()
                })
                    .keyboardType(.numberPad)
            }
            .navigationBarTitle("Add new expense")
            .navigationBarItems(trailing:
                                    Button("Save") {
                                        if amountInputIsValid() {
                                            if let actualAmount = Int(self.amount) {
                                                let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
                                                self.expenses.items.append(item)

                                                self.presentationMode.wrappedValue.dismiss()
                                            }
                                        } else {
                                            showingInvalidAmountAlert = true
                                        }
                                    }
            )
            .alert(isPresented: $showingInvalidAmountAlert, content: {
                Alert(title: Text("Amount Invalid"), message: Text("Make sure to enter a valid number"), dismissButton: .default(Text("OK")) {
                    self.amount = ""
                    self.showingInvalidAmountAlert = false
                })
            })
        }
    }

    private func amountInputIsValid() -> Bool {
        guard let _ = Int(amount) else {
            return false
        }

        return true
    }
}

3      

1- Did you try swiping the row to see if it triggers the delete option?

2- If yes, and the problem persists. Could you reformat the code section so all of it shows? including all functions being called?

3      

MarcusKay,

I am able to swipe and delete, and the Edit/Done button still doesn't show. I updated my code above to include all of the structs and class. Let me know if you need anything else.

Thank you for taking a look at this. :)

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

The issue is in declaring 2 separate navigationBarItems

This is a great example of how usually it's the simple things that we miss. I should have caught it the first time. That's why whenever there's an issue I can't find, I look at everything from the top. 😉

So your leading and trailing buttons should be this:

.navigationBarItems(leading: EditButton(),
                   trailing: Button(action: {
                              self.showingAddExpense = true
                            }) {
                              Image(systemName: "plus")
                            })

Always remember the order of modifiers matters. So if you want both leading and trailing, they have to be on the same modifier.

3      

MarcusKay,

Did you run that and it worked? Made the updated you suggested, which I thought I had tried, and it still doesn't work right (e.g. the Edit button changes to Done, but no red delete dots appear on the left).

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

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

                            Spacer()
                            Text("$\(item.amount)")
                        }
                    }
                    .onDelete(perform: removeItems)
                }
                .navigationBarTitle("iExpense")
                .navigationBarItems(leading: EditButton(), 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)
    }
}

3      

Yes, I copy-pasted your code, changed it and it worked. Here's your content view struct:

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

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

                            Spacer()
                            Text("$\(item.amount)")
                        }
                    }
                    .onDelete(perform: removeItems)
                }
                .navigationBarTitle("iExpense")
                .navigationBarItems(leading: EditButton(),
                                    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)
    }
}

3      

I just tried it on iPod Touch (7th generation) and it didn't work... when I tried on others it didn't work either until I did Clean Build Folder. It then worked on all simulators.

Might be a simulator / Xcode bug.

3      

I think I'm seeing similar behavior.

If I add an expense and then tap Edit the red circles do not show up, but Edit does change to Done in the NavBar. Then if I swipe on any item to expose the row's delete option on the far right side, and then either commit the delete or cancel out of it, then tap edit the delete circles show up.

If I force quit the app and then relaunch it and then immediately hit Edit it behavies as expected.

Does this match what you're seeing?

I'm testing on device with an iPhone 12 Pro Max running iOS 14.3 and using Xcode 12.3.

4      

Change the struct ExpenseItem to

struct ExpenseItem: Identifiable, Codable {
    let id: UUID
    let name: String
    let type: String
    let amount: Double

    init(id: UUID = UUID(), name: String, type: String, amount: Double) {
        self.id = id
        self.name = name
        self.type = type
        self.amount = amount
    }
}

I think it is to do with let id = UUID() and the warning Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

3      

Could be an issue that crept up with iOS 14? I've tried to use the new .toolbar() modifier for this; seems to work better so far.

            .toolbar {
                ToolbarItem(placement: ToolbarItemPlacement.navigationBarLeading) {
                    EditButton()
                }

                ToolbarItem {
                    Button(action: {
                        self.showingAddExpense = true
                    }) {
                        Image(systemName: "plus")
                    }
                }
            }

4      

I got around this by using a CodingKey enum after finding this Stack Overflow question – including the following code in the ExpenseItem struct meant it worked okay.

private enum CodingKeys: String, CodingKey {
    case name, type, amount
}

3      

I am finding it works perfectly when you first run it. Then if you add a new item and dismiss the sheet, the label toggles between Edit and Done and the list bounces sideways. If you then swipe an item sideways to reveal the delete button and then swipe it back, the Edit button starts functioning normally again, putting the list into edit mode and showing the red circle - buttons again!

3      

I've got exactly the same problem here - even when adding the add and edit buttons in the same method, the edit button does not work properly; you can startle it into getting its act together by starting and cancelling a 'swipe- delete' of an item, but it certainly does not work as intended. Bumping this up in case anyone has found a solution...

3      

If you look up navigationBarItems in apple's documentation, it shows that it is deprecated now, and that you should be using toolbar instead. I think that might have something to do with the problems that you are running into with this project. Although, I don't know enough about it to say for certain.

https://developer.apple.com/documentation/swiftui/view/navigationbaritems(leading:)

I can't get that link to work in this forum for some reason, but if you copy and paste it into your browser it should work.

3      

Link to Apple documentation for NavigationBarItems (deprecated)

It may help you to know that when adding the link the text goes between the square brackets [ ], and you replace the https:// between the round brackets ( ) with the actual hyperlink.

4      

I'm finding it works perfectly when you first run it. Then if you add a new item and dismiss the sheet, the label toggles between Edit and Done and the list bounces sideways. If you then swipe an item sideways to reveal the delete button and then swipe it back, the Edit button starts functioning normally again, putting the list into edit mode and showing the red circle - buttons again, I hope this guide will help you!

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.