WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Project 17 Deleting Rows Issue

Forums > 100 Days of SwiftUI

Hi All! I noticed something in project 17 (Flashzilla) that I was 1. wondering if anyone else noticed and 2. can't figure out a solution to.

In this project, there's an EditCards view that appears when the Plus button in ContentView is pressed. This is the screen where you can add and delete individual index cards to and from the array of index cards that you're working with. Deleting cards works as expected if you use swipe to delete and immediately press the delete button, but if you swipe to reveal the delete button and don't immediately press it then the delete button disappears in about half a second. The same goes for if you use an EditButton. In other words, it feels pretty glitchy and not user friendly.

I downloaded and ran Paul's code for this project, and this issue is also present in his code, so it seems like a bug he may have missed before putting the project together.

Here's my code for the EditCards view:

struct EditCards: View {
    @Environment(\.dismiss) var dismiss
    // Custom struct for using the documents directory
    @State private var cards = DataManager.load()
    @State private var newCardPrompt = ""
    @State private var newCardAnswer = ""

    var body: some View {
        NavigationView {
            List {
                Section("Add New Card") {
                    TextField("Question", text: $newCardPrompt)
                    TextField("Answer", text: $newCardAnswer)
                    Button("Add Card", action: addCard)
                }

                // This is where you can see created cards and delete them
                Section {
                    // I tried using ForEach(cards) instead of ForEach(0..<cards.count)
                    // But the issue still persists
                    ForEach(0..<cards.count, id: \.self) { index in
                        VStack(alignment: .leading) {
                            Text(cards[index].prompt)
                                .font(.headline)

                            Text(cards[index].answer)
                                .foregroundColor(.secondary)
                        }
                    }
                    .onDelete(perform: removeCard)
                }
            }
            .navigationTitle("Edit Cards")
            .toolbar {
                Button("Done", action: done)
            }
            .listStyle(.grouped)
        }
    }

    func addCard() {
        let trimmedPrompt = newCardPrompt.trimmingCharacters(in: .whitespaces)
        let trimmedAnswer = newCardAnswer.trimmingCharacters(in: .whitespaces)
        guard trimmedPrompt.isEmpty == false && trimmedAnswer.isEmpty == false else { return }

        let newCard = Card(prompt: trimmedPrompt, answer: trimmedAnswer)
        cards.insert(newCard, at: 0)
        DataManager.save(cards)

        newCardPrompt = ""
        newCardAnswer = ""
    }

    // Method for removing cards
    func removeCard(at offsets: IndexSet) {
        cards.remove(atOffsets: offsets)
        DataManager.save(cards)
    }

    func done() {
        dismiss()
    }
}

My Card type:

struct Card: Codable, Identifiable, Hashable {
    var id = UUID()
    let prompt: String
    let answer: String

    static let example = Card(prompt: "Who played the 13th Doctor in Doctor Who?", answer: "Jodie Whittaker")
}

   

I replaced this line in your code above:

// @State private var cards = DataManager.load()
// replace above with this
@State private var cards = [Card.example, Card.example, Card.example]  // dummy array of cards.

I also commented out the calls to DataManager.save()

// Comment these to test.
// DataManager.save(cards)

Then I ran your code in the simulator. Works fine! Swipe to delete will delete the fake row with a full swipe. A partial swipe shows the delete button. But it stays put and doesn't exhibit the behavior you mention.

So, if this debugging session provides you with any comfort, it's that the EditCards view may not be the culprit. In your FlashCardApp view change your code to start with the EditCards() view. Test the results for yourself.

@main
struct FlashCardApp: App {
    var body: some Scene {
        WindowGroup {
            EditCards()  // <------ Change this to focus on EditCards() view
        }
    }
}

Let us know what you find!

   

Thanks a lot for the response!

Like you suggested, I tried making EditCards to be the main view that appears within the FlashCardApp file and it worked! However, I also tried commenting out the call to the DataManager.save() method and adding in a Dummy array of cards like you suggested without adding EditCards to the FlashCardApp file and then the issue still persisted. In other words, it seems I don't have to follow all of your suggestions to solve the problem, I only need to change up the FlashCardApp app file in the way that you described.

Would you mind explaining why this works? I'm glad this solution solves the problem, but it doesn't seem like a very ideal fix, especially since it means I don't get any control over the view that's shown when the app starts up. I just don't understand how changing the order in which a view presents itself could affect whether or not deleting rows within that view works properly. I'm guessing there's more at play here than just changing the order that the view shows up, but regardless it seems quite strange.

   

It is due to the timer being published after saving a set of cards, even when in the edit mode, and interferring with the delete. It should be cancelled during editing.

Using Paul's solution as the baseline.

in ContentView

change the timer definition to be

@State private var timer = Timer.publish(every: 1, on: .main, in: .common)

Add this function above the var body:

private func pauseTimerWhilstEditing() {
    self.timer.connect().cancel()
    showingEditScreen = true
}

Change the operation for the add button to be

Button {
    pauseTimerWhilstEditing()
} label: {

Add the following lines to the end of resetCards()

self.timer = Timer.publish(every: 1, on: .main, in: .common)
_ = self.timer.connect()

   

I wasn't really trying to solve your problem. I was just isolating a portion of your solution and testing that one piece.

Since I didn't have access to your data, or your JSON loading code, I didn't have the DataManager object. Instead, i cheated. I created a dummy array of Card objects using Card.example from the Card struct.

Additionally, since I didn't have the DataManager object, I couldn't call the DateManager.save() function. I wasn't really interested in saving data to device anyway. I was trying to replicate your interface issue. So, I commented out the save function. Adding and deleting from the array still works. The isolated code just doesn't save it to device.

I isolated the EditCard view from the rest of your application. My observation is the EditCard view seems to work fine! I had hoped you'd try isolating it and verifying the results for yourself.

If you eliminate EditCard as the source of your interface issues, then you'll need to look for the problem in other views.

Update:

@greenamber found the issue in another part of your code. I was just sharing a technique for isolating and testing the view where you thought you had the problem.

   

@Obelix That makes a lot of sense! I hadn't thought to troubleshoot that way (still pretty new here!) so it really didn't occur to me that that's what you were trying to get me to do, sorry for the confusion there!

@Greenamberred Thanks a lot for taking the time! I'm gonna give this a shot

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

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

Reply to this topic…

You need to create an account or log in to reply.

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.