Представление не обновляется при использовании фонового актера для записи данных в контейнер

В настоящее время я изучаю 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()
    }
    
}

Проблема с вызовом вставки, а затем сохранения заключается в том, что вы также сохраняете любые другие изменения, внесенные в контекст. Вот почему обычно контекст сохраняется, когда приложение приостанавливается, чтобы оно могло сохранить все изменения за один раз. Тогда вам не понадобится фоновый контекст для этих простых вставок. Для длительных вставок вы можете создать новый контекст для каждой асинхронной задачи, однако, вероятно, возникнет та же проблема, что пользовательский интерфейс не будет обновляться, поэтому вам придется пойти по маршруту постоянной истории.

malhal 29.07.2024 19:58
Стоит ли изучать 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
1
62
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это связано с ошибкой в ​​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
}

Я попробовал, но получил ошибку. Возврат из инициализатора без инициализации всех сохраненных свойств.

Damiano Miazzi 30.07.2024 16:16

Я обновил ответ другим решением

Joakim Danielson 30.07.2024 17:06

Я обновляю свой вопрос, добавляя более простой код, а ваш код все еще не работает: я вижу изменения только после того, как закрою и снова открою приложение. Как я могу обновить пользовательский интерфейс после изменения.

Damiano Miazzi 01.08.2024 04:54

Ваше последнее изменение меня устраивает, и пользовательский интерфейс обновляется.

Joakim Danielson 01.08.2024 08:51

Это нехорошее решение: вы заблокируете пользовательский интерфейс и можете вызвать гонку данных. @ModelActor предназначен для использования в фоновом режиме, сейчас я сталкиваюсь с той же самой проблемой и до сих пор не смог найти решение

ia244 01.08.2024 19:34

@ ia244 да, конечно, это заблокирует пользовательский интерфейс, поскольку мы намеренно запускаем главного актера, чтобы обойти ошибку в SwiftData. Что-то я написал в ответ, поэтому я не уверен, почему вы жалуетесь (и отрицаете), поскольку я четко указал на недостатки решения.

Joakim Danielson 01.08.2024 19:58

Вы не упомянули об этом, вы просто упомянули, что это будет связано с главным действующим лицом, вам следует отредактировать свой ответ, кроме того, очевидно, что вопрос предназначался для фоновых задач.

ia244 01.08.2024 20:00

@JoakimDanielson посмотрите мой ответ на обходной путь, предложенный одним из инженеров/разработчиков Apple.

ia244 01.08.2024 20:48

@ ia244 Я не тестировал ваше решение, но оно выглядит лучше для действительно длительных фоновых задач, хотя я думаю, что мое решение больше подходит для простых операций, таких как одиночные вставки или удаления. Я думал, что ясно изложил то, что написал в своем ответе, но, возможно, вы правы, и я мог бы быть яснее.

Joakim Danielson 01.08.2024 21:04
Ответ принят как подходящий

Мы столкнулись с той же проблемой: после нескольких дней ее изучения, похоже, это проблема с 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().

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