«Неотправляемый тип при неявном асинхронном доступе» — предупреждение при доступе к основному контексту в AppIntent

Цель

Я пытаюсь одновременно использовать SwiftData и Widgets. Я создал новый проект на основе текущего шаблона SwiftData Xcode 15.4. Языковая версия Swift, установленная в Xcode, — 5.

Моя цель сейчас — иметь возможность добавлять новый элемент в ModelContext из виджета.

Что я реализовал на данный момент

Я добавил цель виджета и намерение, позволяющее добавить элемент в контекст виджета. Чтобы получить доступ к ModelContext из цели виджета, я нашел DataModel.swift в этом примере проекта от Apple, который я скачал.

Содержимое этого файла следующее:

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

Я получаю доступ к этому с намерением следующим образом:

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

Проблема

Все работает как положено, но я получаю следующее предупреждение: Non-sendable type 'ModelContext' in implicitly asynchronous access to main actor-isolated property 'mainContext' cannot cross actor boundary.

Что нужно сделать, чтобы избавиться от этой ошибки (которая, скорее всего, будет ошибкой в ​​Swift 6, я думаю…)?


1
50
2

Ответы:

Поскольку ModelContext в любом случае должен работать на главном актере, отправьте всю функцию главному актеру. И объявите title константой, чтобы избежать еще одной ошибки/предупреждения.

struct AddNewItemIntent: AppIntent {
    static let title: LocalizedStringResource = "Add new item intent"
    
    @MainActor
    func perform() async throws -> some IntentResult {
        print("AddNewItemIntent button tapped")
        let newItem = Item(timestamp: Date())
        DataModel.shared.modelContainer.mainContext.insert(newItem)
        return .result()
    }
}

Решено

perform не изолирован от главного актера, поэтому вы не можете безопасно и асинхронно получить доступ к изолированному главному актеру mainContext.

Поскольку DataModel уже является actor, я бы заменил его на @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)
    }
}

Обратите внимание, что вы не можете напрямую сделать что-то вроде await model.modelContext.insert(newItem) в perform, поскольку Item не является Sendable. Вы должны убедиться, что отправляете Sendable вещи только туда и обратно DataModel.

Вместо этого вы можете, например. напишите метод в DataModel под названием insert(itemWithTimestamp:), который принимает только Date, то есть Sendable.

func insert(itemWithTimestamp timestamp: Date) {
    modelContext.insert(Item(timestamp: timestamp))
}

Чтобы упростить задачу, я написал метод run (см. выше), который вы можете использовать для запуска кода, изолированного от DataModel, поэтому в perform вы можете написать:

await DataModel.shared.run { model in
    let newItem = Item(timestamp: Date())
    model.modelContext.insert(newItem)
}

Вы создаете newItem внутри контекста, изолированного от актера, поэтому вам не нужно отправлять его в DataModel.

Обратите внимание, что все, что захватывает замыкание run, также должно быть Sendable.