SwiftData: Solving Fatal Errors and EXC_BAD_ACCESS While Handling Entities on Different Threads
When SwiftData was first announced I considered leaving it alone before realizing this may be the best time to switch over. That said, I've had some difficulties getting the same functionalities working, like my earlier post where I had issues filtering the browse lists by an entity relationship, but having my app crash at random-seeming parts when creating entities on background threads was, for me, the most opaque and frustrating one yet... at least for now. And, once again, like all issues it was resolved with a simple solution.
Back Story
Part of my back end code is written in a class that is composed fully of asynchronous functions that I call awaiting the returned results. Previously with CoreData I pass in a main entity and the data context to the method where new entities, all related and connected to that main entity, are created and saved.
This has worked well in the past although it didn't act consistently once I updated my code to the new SwiftData methodology and dropped the data context as the ModelContext
could be found through that passed in main entity.
But First Concurrency
First a quick aside in case you're also looking to speed up your code. To speed up my code I ended up using two posts I found on Donny's blog. The first post, which I started out on, used async let
to run your steps serially rather than concurrently while the second post was all about running tasks in parallel with task groups. I came across these while trying to solve my problem and although the new code compiled and ran so much quicker my issues weren't fixed and, in fact, it even broke my partial (and temporary) fix of adding a simple sleep
command. I used the sleep
command to prevent the race conditions I thought, at first, were causing the issues.
Word of warning: one spot I updated didn't work with task groups (didn't create all the needed entities) even once I fixed the main issue so I went back to the working async let
there and it then worked beautifully. The other spot I used task groups is still set up and working perfectly though.
In addition to those posts I also found Swift Senpai's post Understanding Swift Task Groups With Example that looked great at a glance in case you want another explanation.
The Problems
EXC_BAD_ACCESS
The first error I had was a generalized EXC_BAD_ACCESS (code=1, address=...)
that showed up mainly in my entry App @Main
struct but also popped up at various entity relationships spots all under various thread count numbers.
I vaguely remembered a list of common SwiftData errors on Hacking with Swift so I tracked down his post Common SwiftData errors and their solutions where he sums up the EXC_BAD_ACCESS (code=1, address=0x0)
well in the very first sentence with:
"If this occurs you're in trouble, because it could mean a whole range of things."
He then went on to describe how to fix this issue when it shows up in a predicate which didn't help me here or with my actual current, at that time, predicate issue.
When further researching this I came across Antoine Van Der Lee's blog SwiftLee that explained using an address sanitizer once the source of the EXC_BAD_ACCESS was found locally along with another post on using the thread sanitizer to deal with data races. These couldn't be used at the same time and I found they didn't personally help me as anytime I had one of them enabled my app performed beautifully. Figured I'd share this with you though in case it could help you.
Fatal Error: Duplicate Keys
At some point in all of this when I couldn't figure out how to fix the issue I decided to at least help mitigate it by manually saving the ModelContext
after each related entity was done being created. I figured this way if the program crashed at least the data that had been created before that point would be saved and the user wouldn't have to start back at the beginning each time. This didn't help as once I added saving, at any of the points, I got fatal errors on my entities complaining about duplicate keys.
Fatal error: Duplicate keys of type 'EntityName' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or that members of such a dictionary were mutated after insertion.
The Solution
Path to Solution
The StackOverflow question titled SwiftData insert crashes with EXC_BAD_ACCESS using background thread from ModelActor got me to where I needed to be. That said, I need to be completely honest and share that I didn't read the code section of the question and the approved answer was just scanned. What helped me specifically was the solution added to bottom of the question and in that solution mainly just the first sentence: "You have to create the ModelActor on a different thread than the main tread."
This line immediately reminded me of Hacking with Swift's post How to discard changes to a SwiftData object where he showed how to make an entity editable in a way where you can choose when to save or discard any changes instead of the default autosaving. Earlier I had followed this code to implement it in my edit views so a user can roll everything back if they choose to cancel (by just dismissing it all) or keep the changes by saving the ModelContext
manually.
Solution With Example
With this idea I decided to update the problematic functions so rather than passing in the main entity itself I instead pass in the ModelContainer
and the entity's PersistentIdentifier
so it can be recreated within a new ModelContext
for just this thread.
Assuming you're using async let
I created a quick code example to show how I fixed my problem. This also worked in my function using TaskGroup
too.
In this example I have a main function where I pass in my main entity (to connect with any other created entities) and an array of information to go through. This method just iterates through the array and calls the asynchronous method making sure to pass the entity by its PersistentModelID
and send in the ModelContainer
.
public func mainEntryMethod(entity: EntityName, arrayToGoThrough: [objectType]) async {
for thisObject in arrayToGoThrough {
async let _ = createEntities(in: entity.modelContext!.container, entityID: entity.persistentModelID, thisObject: objectType)
}
}
This asynchronous method is where the magic happens. Here the main entity is recreated in a new ModelContext
based on the passed in ModelContainer
and if it can't be recreated I exit the method. If it works I use the information passed in to create what I originally wanted before saving the whole thing and returning.
private func createEntities(in container: ModelContainer, entityID: PersistentIdentifier, objectInfo: objectType) async {
// First recreate the main entity in it's own ModelContext
let modelContext = ModelContext(container)
modelContext.autosaveEnabled = false
let entity = modelContext.model(for: entityID) as? EntityName
if entity == nil {
// Can't continue without entity
// TODO: Handle any error handling and...
return
}
// ... creating the related entities and attaching them
do {
try modelContext.save()
} catch {
// It can't be saved
// TODO: Handle any error handling and...
return
}
// Success!
}
With the different ModelContext
used in my problematic asynchronous methods both my fatal and bad access errors are gone!
I hope this solution helps you solve your problem. If it didn't quite get you all the way I'd love to hear, in the comments below, how you solved it! Maybe your solution can help someone else!
I hope you’re having a good day.
If you’re interested in getting any of my future blog updates I normally share them to my Facebook page and Instagram account. You’re also more than welcome to join my email list located right under the search bar or underneath this post.