0

I have a SwiftUI calendaring app with a UI similar to the built-in Calendar.app. I'm getting crashes whenever I try to delete events. The overall lifecycle of my app is as follows:

  • Download calendar data from server and populate models ([Events], [Users], [Responses] etc)
  • Transform the source data into a more structured format (see https://stackoverflow.com/a/58583601/2282313)
  • Render list view of events, each event linking to a Detail View and an Edit modal (very similar to calendar.app)
  • When an event is deleted, I tell the server to delete the event (if it's a recurring event, the server will delete multiple events), then refresh my data from the server by re-downloading the data, re-populating the models and re-generating the structured data (which causes the list to refresh).

When I do this, I get crashes coming from my calculated values because event data displayed in the detail view is no longer available. For example, I get the array index of a user's RSVP as follows:

    var responseIndex: Int {
        userData.responses.firstIndex(where: { $0.user == response.user && $0.occurrence == response.occurrence })!
    } 

I thought this was because I hadn't dismissed the view displaying the deleted event before updating the data, but even if I delay the data refresh until the view is no longer displayed, I still get the crash (SwiftUI seems to keep these views in memory).

What is the right way to handle data deletion? Do I need to keep deleted events in my UserData EnvironmentObject and just mark them as "deleted/hidden" to avoid this issue, or is there a better way to handle it?

There's quite a bit of code involved in this, so it's tricky to provide a sample I'm happy to add relevant bits if asked.

Spielo
  • 481
  • 1
  • 6
  • 17
  • Im having a similar issue, nothing seems to work. – Eye of Maze Nov 28 '19 at 21:31
  • @SybrSyn I'm working on re-writing my code so that instead of deleting any UserData, I just find events which have been deleted and mark them as such. I'm also having to re-jig some things so that I'm always using the structured data as the source of truth (which is good!). Once I have it working, I'll share what I've done, but there's probably a better way to do it that I haven't figured out. – Spielo Nov 28 '19 at 21:45

1 Answers1

0

EDIT: I found this article which clarifies something really well: https://jasonzurita.com/swiftui-if-statement/

SwiftUI is perfectly happy to try and render nil views, it just draws nothing. Counter-intuitively, a good way to avoid crashes and make the compiler happy is to set your code up around this.

Original "answer" follows...

I don't know if this is the "right" way to do this, but I ended up making sure that none of my UserData is ever deleted to avoid the crashes. I added a "deleted" bool to my Occurrence (i.e. Event) object, and when I refresh my structured data, I get the latest data from the server, but check to see if any of the old ones are no longer present. Steps are:

  1. Get latest list of occurrences from server
  2. Create a second init() for my structured data which takes the existing data as an argument
  3. Inside the new init(), flatten the structured data, check for deleted items against the new data, update data which hasn't been removed, cull duplicates, then merge in net new data. Once that's done, I call my original init() with the modified data to create new structured data

Code looks like this:

init(occurrences: [Occurrence], existing: [Day]) {

    // Create a mutable copy of occurrences (useful so I can delete duplicates)
    var occurrences = occurrences

    // Flatten the structured data into a plan array of occurrences again
    var existingOccurrences = existing.compactMap({ $0.occurrences }).flatMap { $0 }

    // Go through existing occurrences and see if they still exist.
    existingOccurrences = existingOccurrences.map {
        occurrence -> Occurrence in
        let occurrenceIndex: Int? = occurrences.firstIndex(where: { $0.id == occurrence.id })
        // If the occurrence no longer exists, mark it as "deleted" in the original data
        if  occurrenceIndex == nil {
            var newOccurrence = occurrence
            newOccurrence.deleted = true
            return newOccurrence
            // If it still exists, replace the existing copy with the new copy
            // (in case it has changed since the last pull from the server)
            // Remove the event from the "new" data so you don't get duplicates
        } else {
            let newOccurrence = occurrences[occurrenceIndex!]
            occurrences.remove(at: occurrenceIndex!)
            return newOccurrence
        }
    }

    // Merge the existing data (with deleted items marked) and the updated data (with deleted items removed)
    let finalOccurrences = existingOccurrences + occurrences

    // Re-initialize the strutured data with the new array of data
    self = EventData(occurrences: finalOccurrences)
}

Once this was done, I had to update my code to make sure I'm always using my structured data as the source of truth (which I wasn't doing before because accessing the "source" flat data was often easier, and I've updated my ForEach in my list view to only render a row if deleted is false.

It works! It's perhaps a sub-optimal way to solve the problem, but no more crashes. Still interested to hear better ways to solve the problem.

Spielo
  • 481
  • 1
  • 6
  • 17