I believe I now know exactly what's causing this.
I've filed FB14344675 with a detailed description of the problem.
When an @ModelActor
is created and later released (for example dropped at the end of a function scope), the model instances fetched by its associated model context can't be meaningfully used anymore.
This behavior is new in iOS 18. I don't know whether iOS 18 now actually resets the background context on deinit, whether iOS 17 somehow kept it alive until all instances were gone, or whether it's something entirely different that caused this regression.
Anyway, the solution is as follows:
- Do NOT fetch model instances using a
ModelActor
-conforming actor
, if the actor
doesn't outlive the fetched instances. - Instead, keep the
actor
around. I'm doing this using a @State
variable, but I suppose it could also be done by making the actor a singleton.
In the case that the crash is actually indented behavior and model instances aren't supposed to outlive their model context, I'd be very interested in hearing about a proper architectural solution to this problem.
The singleton solution seems the simplest, but it makes two assumptions that I don't like at all: The model context has to be well-known and static.
In my case, this simply doesn't work, because I have two model containers: One for previews with in-memory storage only, and one for development with proper on-disk storage.
Since the "correct" model container is only known at runtime, my actor
can't be a singleton. It has to be instantiated with a reference to the model context.
This, however, causes the following problem: The actor can't be created on view creation, because the model container isn't (yet) accessible. I don't see a good solution to this.
This has been my solution so far:
struct MyView: View {
@Environment(\.modelContainer) private var container
@State private var myActor: MyModelActor?
// Use this whenever you need the actor
func getModelActor() -> MyModelActor {
guard let myActor else {
let myActor = MyModelActor(modelContainer: container)
self.myActor = myActor
return myActor
}
return myActor
}
var body: some View {
SomeContent()
}
}
That's a super ugly solution in my opinion, and although it does work, I really wouldn't wanna use this if there's a better way.