Я пытаюсь одновременно использовать 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, я думаю…)?
Поскольку 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
.