В настоящее время я изучаю SwiftData. Я хочу записывать и сохранять данные в свое приложение-контейнер из ContentView и в то же время с помощью modelActor я также хочу сохранять данные из фонового потока.
Насколько я понимаю, в любом случае мое представление должно обновляться, когда происходят изменения, но этого не происходит, когда я сохраняю данные из фона.
Что не так в моем коде:
//
// ContentView.swift
// Test-SwiftData
//
// Created by Damiano Miazzi on 01/08/2024.
//
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
HStack{
Text(item.text)
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem {
Button {
Task{
let act = DataManager(modelContainer: modelContext.container)
await act.insert()
}
} label: {
Text("Add BK")
}
}
}
} detail: {
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date(), text: "Main")
modelContext.insert(newItem)
try? modelContext.save()
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
}
@Model
final class Item {
var timestamp: Date
var text: String
init (timestamp: Date = Date(), text: String) {
self.timestamp = timestamp
self.text = text
}
}
@main
struct Test_SwiftDataApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
@ModelActor
actor DataManager {
@MainActor
public init(modelContainer: ModelContainer,mainActor _: Bool) {
let modelContext = modelContainer.mainContext
modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
self.modelContainer = modelContainer
}
func insert(){
let item = Item(timestamp: Date(), text: "BK")
modelContext.insert(item)
try? modelContext.save()
}
}
Это связано с ошибкой в SwiftData: вам нужно использовать ModelContext, привязанный к основному актеру.
Использование главного актера, конечно, не оптимально, но это временное решение, пока эта ошибка не будет исправлена, и если актер используется для простых операций, таких как вставка, обновление или удаление отдельных объектов, это должно быть незначительной проблемой.
В качестве обходного пути на данный момент вы можете изменить инициализацию DataManager
init(with container: ModelContainer) {
context = container.mainContext
}
Для actor
я думаю, вам нужно удалить свойство modelContext, поскольку оно в любом случае добавляется макросом, и вместо этого инициализировать его следующим образом. Обратите внимание, что мы используем @MainActor
и свойство mainContext
из переданного контейнера.
@MainActor
public init(modelContainer: ModelContainer, mainActor _: Bool) {
let modelContext = modelContainer.mainContext
modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
self.modelContainer = modelContainer
}
Я попробовал, но получил ошибку. Возврат из инициализатора без инициализации всех сохраненных свойств.
Я обновил ответ другим решением
Я обновляю свой вопрос, добавляя более простой код, а ваш код все еще не работает: я вижу изменения только после того, как закрою и снова открою приложение. Как я могу обновить пользовательский интерфейс после изменения.
Ваше последнее изменение меня устраивает, и пользовательский интерфейс обновляется.
Это нехорошее решение: вы заблокируете пользовательский интерфейс и можете вызвать гонку данных. @ModelActor предназначен для использования в фоновом режиме, сейчас я сталкиваюсь с той же самой проблемой и до сих пор не смог найти решение
@ ia244 да, конечно, это заблокирует пользовательский интерфейс, поскольку мы намеренно запускаем главного актера, чтобы обойти ошибку в SwiftData. Что-то я написал в ответ, поэтому я не уверен, почему вы жалуетесь (и отрицаете), поскольку я четко указал на недостатки решения.
Вы не упомянули об этом, вы просто упомянули, что это будет связано с главным действующим лицом, вам следует отредактировать свой ответ, кроме того, очевидно, что вопрос предназначался для фоновых задач.
@JoakimDanielson посмотрите мой ответ на обходной путь, предложенный одним из инженеров/разработчиков Apple.
@ ia244 Я не тестировал ваше решение, но оно выглядит лучше для действительно длительных фоновых задач, хотя я думаю, что мое решение больше подходит для простых операций, таких как одиночные вставки или удаления. Я думал, что ясно изложил то, что написал в своем ответе, но, возможно, вы правы, и я мог бы быть яснее.
Мы столкнулись с той же проблемой: после нескольких дней ее изучения, похоже, это проблема с iOS 18 и SwiftData, согласно этому https://developer.apple.com/forums/thread/759364 исправление находится в стадии разработки. сделано, ниже вы найдете временное решение, предложенное инженером Apple:
В качестве обходного пути до того, как проблема будет устранена на стороне платформы, вы можете рассмотреть возможность наблюдения за уведомлением .NSManagedObjectContextDidSave
и запуска обновления SwiftUI из обработчика уведомлений. Например:
import Combine
extension NotificationCenter {
var managedObjectContextDidSavePublisher: Publishers.ReceiveOn<NotificationCenter.Publisher, DispatchQueue> {
return publisher(for: .NSManagedObjectContextDidSave).receive(on: DispatchQueue.main)
}
}
struct MySwiftDataView: View {
@Query private var items: [Item]
// Use the notification time as a state to trigger a SwiftUI update.
// Use a state more appropriate to your app, if any.
@State private var contextDidSaveDate = Date()
var body: some View {
List {
ForEach(items) { item in
Text("\(item.timestamp)")
}
// Refresh the view by changing its `id`.
// Use a way more appropriate to your app, if any.
.id(contextDidSaveDate)
}
.onReceive(NotificationCenter.default.managedObjectContextDidSavePublisher) { _ in
contextDidSaveDate = .now
}
}
}
Это обновит каждое представление, к которому вы прикрепите contextDidSaveDate
, как .id(contextDidSaveDate)
при выполнении контекста save()
.
Проблема с вызовом вставки, а затем сохранения заключается в том, что вы также сохраняете любые другие изменения, внесенные в контекст. Вот почему обычно контекст сохраняется, когда приложение приостанавливается, чтобы оно могло сохранить все изменения за один раз. Тогда вам не понадобится фоновый контекст для этих простых вставок. Для длительных вставок вы можете создать новый контекст для каждой асинхронной задачи, однако, вероятно, возникнет та же проблема, что пользовательский интерфейс не будет обновляться, поэтому вам придется пойти по маршруту постоянной истории.