I am trying to get my feet wet in both SwiftData and Widgets at the same time. I have created a new project from the current Xcode 15.4 SwiftData template. Swift language version set up in Xcode is 5.
My goal now is to be able to add a new item to the ModelContext
from a widget.
I added the Widget target and an intent enabling me to add an item to the context from the widget. To get access to the ModelContext
from the Widget target, I have found DataModel.swift
in this sample project from Apple that I downloaded.
Content of that file is this:
import SwiftUI
import SwiftData
actor DataModel {
struct TransactionAuthor {
static let widget = "widget"
}
static let shared = DataModel()
private init() {}
nonisolated lazy var modelContainer: ModelContainer = {
let modelContainer: ModelContainer
do {
modelContainer = try ModelContainer(for: Item.self)
} catch {
fatalError("Failed to create the model container: \(error)")
}
return modelContainer
}()
}
I am accessing that from the intent like this:
import AppIntents
struct AddNewItemIntent: AppIntent {
static var title: LocalizedStringResource = "Add new item intent"
func perform() async throws -> some IntentResult {
print("AddNewItemIntent button tapped")
let newItem = Item(timestamp: Date())
await DataModel.shared.modelContainer.mainContext.insert(newItem)
return .result()
}
}
Everything works as expected–but I get a the following warning:
Non-sendable type 'ModelContext' in implicitly asynchronous access to main actor-isolated property 'mainContext' cannot cross actor boundary
.
What needs to be done to get rid of this error (which will most likely be an error in Swift 6 I guess …)?
perform
is not isolated to the main actor, so you cannot safely and asynchronously access the main actor-isolated mainContext
.
Since DataModel
is already an actor
, I would change it to a @ModelActor
.
@ModelActor
actor DataModel {
static let shared = DataModel()
private init() {
do {
let modelContainer = try ModelContainer(for: Item.self)
// 'init(modelContainer:)` is an initialiser generated by the @ModelActor macro
self.init(modelContainer: modelContainer)
} catch {
fatalError("Failed to create the model container: \(error)")
}
}
func run<Result: Sendable>(block: @Sendable (isolated DataModel) async throws -> Result) async rethrows -> Result {
try await block(self)
}
}
Note that you cannot directly do something like await model.modelContext.insert(newItem)
in perform
, since Item
is not Sendable
. You should ensure that you only send Sendable
things to and from DataModel
.
Instead, you can e.g. write a method in DataModel
called insert(itemWithTimestamp:)
that only takes a Date
, which is Sendable
.
func insert(itemWithTimestamp timestamp: Date) {
modelContext.insert(Item(timestamp: timestamp))
}
To make things easier, I have written a run
method (see above) that you can use to run code that isolated to DataModel
, so in perform
you can write:
await DataModel.shared.run { model in
let newItem = Item(timestamp: Date())
model.modelContext.insert(newItem)
}
You create newItem
inside the actor-isolated context, so you don't need to send it to DataModel
.
Note that everything the run
closure captures should also be Sendable
.