«Неотправляемый тип при неявном асинхронном доступе» — предупреждение при доступе к основному контексту в 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, я думаю…)?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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()
    }
}

Я могу подтвердить, что это работает, поэтому это правильное решение проблемы, спасибо! Я выбираю ответ выше в качестве решения, потому что мне кажется, что это еще более лаконичный подход для разных случаев использования (кстати, var против let не представляет никакой сложности, но он даже не предупреждает…)

appfrosch 22.08.2024 09:49

Нет проблем 😉. Предупреждение var зависит от уровня совместимости Swift 6 и со временем может стать проблемой.

vadian 22.08.2024 10:04
Ответ принят как подходящий

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.

Я могу подтвердить, что это избавляет от предупреждения и кажется хорошим централизованным подходом для различных случаев использования. Ценю ваше объяснение 🙌

appfrosch 22.08.2024 09:51

Другие вопросы по теме