I'm working on an app that uses the AppIntents framework to write to a SwiftData persistence. I add the Sendable shared model container to app dependency manager that my intents and entities and queries can use as a dependency.
When I run the app in Swift 5 language mode, the app works without a problem. However, when I change to Swift 6 language mode, the app crashes at runtime at the point where the dependency should be resolved. In both language modes, strict concurrency checking is set to complete.
I've reduced the app to this MVP:
import AppIntents
import SwiftData
import SwiftUI
@main
struct AppDependencyBugApp: App {
private let modelContainer: ModelContainer
init() {
let modelContainer = ModelContainer.shared
self.modelContainer = modelContainer
AppDependencyManager.shared.add(dependency: modelContainer)
}
var body: some Scene {
WindowGroup {
ItemsList()
.modelContainer(modelContainer)
}
}
}
struct ItemsList: View {
@Query private var items: [Item]
var body: some View {
NavigationStack {
List {
Section {
Button("New Item", systemImage: "plus.circle", intent: NewItemIntent())
}
ForEach(items) { item in
VStack(alignment: .leading) {
Text(item.dateCreated, format: .iso8601)
Text(item.id.uuidString)
.monospaced()
.font(.caption)
}
}
}
.navigationTitle("Items")
}
}
}
struct NewItemIntent: AppIntent {
static var title: LocalizedStringResource {
"New Item Intent"
}
@Dependency private var modelContainer: ModelContainer
func perform() async throws -> some IntentResult {
let act = DivvydeModelActor(modelContainer: modelContainer)
try await act.newItem()
return .result()
}
}
extension ModelContainer {
@MainActor
static let shared: ModelContainer = {
let container: ModelContainer
do {
let schema: Schema = .init([Item.self])
let configuration: ModelConfiguration = .init(schema: schema)
container = try ModelContainer(for: schema, configurations: configuration)
} catch {
preconditionFailure("Model container failed to initialize with error: \(error.localizedDescription)")
}
container.mainContext.autosaveEnabled = false
return container
} ()
}
@ModelActor
final actor DivvydeModelActor {
@MainActor
func newItem() async throws {
let modelContext = modelContainer.mainContext
let item = Item()
modelContext.insert(item)
try withAnimation {
try modelContext.save()
}
}
}
@Model
final class Item: Identifiable {
private(set) var id: UUID
private(set) var dateCreated: Date
convenience init(date: Date = .now) {
self.init(id: .init(), dateCreated: date)
}
private init(id: ID, dateCreated: Date) {
self.id = id
self.dateCreated = dateCreated
}
}
Am I missing something out of the Swift concurrency picture or if this is a bug in the framework?
Define the @.Depdendency with a key and declare an async / @MainActor closure and pass that as the dependency, e.g.
@Dependency(key: "Container")
private var result: ModelContainer
@main
struct MyApp: App {
init() {
let asyncDependency: () async -> (ModelContainer) = { @MainActor in
return ModelContainer.shared
}
AppDependencyManager.shared.add(key: "Container", dependency: asyncDependency)
MyAppShortcuts.updateAppShortcutParameters()
}
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(ModelContainer.shared)
}
}
}
There are 3 .add(depdendency:) funcs that have the same param names (whose genius idea was that? ;-) and it seems to me the @autoclosure crash at runtime in Swift 6:
final public func add<Dependency>(key: AnyHashable? = nil, dependency dependencyProvider: @autoclosure @escaping () -> Dependency) where Dependency : Sendable
final public func add<Dependency>(key: AnyHashable? = nil, dependency dependencyProvider: @autoclosure @escaping () -> () throws -> Dependency) where Dependency : Sendable
final public func add<Dependency>(key: AnyHashable? = nil, dependency dependencyProvider: @escaping () async throws -> Dependency) where Dependency : Sendable
Using the async closure dependency adding should fix the issue, but I'm having to put this issue aside anyway because conformance to EntityPropertyQuery isn't Swift 6 compliant anyway.
I also ran into this issue. As far as I can see this is a bug in AppDependencyManager’s API definition, though fortunately there is an easy workaround.
The issue
Auto-closure variants of add are non-async. However they don’t require these to be @Sendable or otherwise specify their isolation rule. Therefore, any use of these closures apart from within the actor isolation they were created in is a violation of the API contract.
Now if we create this non-sendable closure with MainActor isolation (e.g. within App.init), the compiler will emit precondition checks in the closure to make sure it is always used on the MainActor. This causes the app to crash when AppIntents framework calls it from a background queue.
Note that we can see this issue even if we create a wrapper for add but give it the same signature (shim1 in sample code below).
Also note that the reason the async variant of the API does not have this problem (as mentioned previously) is because it has well-defined isolation rules.
I’ve raised a bug for this (FB15219224).
Workaround
As far as I understand, technically the violation is always there. But we can suppress the precondition check and therefore the crash in a couple of ways:
Give enough information to the compiler that the closure will in fact needs to be @Sendable – See shim2.
Only create the closure in a context that is not isolated to the main actor – See shim3.
Somehow force the compiler to use the async version of the API as @prathameshk already mentioned.
@main
struct AppDependencyManagerBugApp: App {
init() {
let dependency = MyDependency()
// Using this will cause a crash
AppDependencyManager.shared.add(dependency: dependency)
// Using this will cause a crash
// AppDependencyManager.shared.shim1(dependency: dependency)
// Using this will work fine
// AppDependencyManager.shared.shim2(dependency: dependency)
// Using this will work fine
// AppDependencyManager.shared.shim3(dependency: dependency)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
private extension AppDependencyManager {
// Shim using exact declaration as ``AppDependencyManager.add``
func shim1<Dependency>(key: AnyHashable? = nil, dependency dependencyProvider: @escaping () -> Dependency) where Dependency : Sendable {
add(key: key, dependency: dependencyProvider())
}
// Shim using exact declaration as ``AppDependencyManager.add``, except that `dependencyProvider` is marked as `Sendable`
func shim2<Dependency>(key: AnyHashable? = nil, dependency dependencyProvider: @Sendable @autoclosure @escaping () -> Dependency) where Dependency : Sendable {
add(key: key, dependency: dependencyProvider())
}
// Shim without using an autoclosure
func shim3<Dependency>(key: AnyHashable? = nil, dependency: Dependency) where Dependency : Sendable {
add(key: key, dependency: dependency)
}
}