swiftswiftdata

"Non-sendable type in implicitly asynchronous access"-warning when accessing the mainContext in an AppIntent


The goal

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.

What I have implemented so far

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()
  }
}

The issue

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 …)?


Solution

  • 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.