Как убедиться, что представление WidgetKit показывает правильные результаты от @FetchRequest?

У меня есть приложение, использующее Core Data с CloudKit. Изменения синхронизируются между устройствами. Основная цель имеет возможность фоновых режимов с отмеченными удаленными уведомлениями. Основная цель и цель виджета имеют одну и ту же группу приложений, и обе имеют возможность iCloud с установленными службами CloudKit и одним и тем же контейнером в контейнерах.

Моя цель - отображать актуальные записи Core Data в представлении SwiftUI WidgetKit.

Целевой файл моего виджета:

import WidgetKit
import SwiftUI
import CoreData

// MARK: For Core Data

public extension URL {
    /// Returns a URL for the given app group and database pointing to the sqlite database.
    static func storeURL(for appGroup: String, databaseName: String) -> URL {
        guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
            fatalError("Shared file container could not be created.")
        }
        
        return fileContainer.appendingPathComponent("\(databaseName).sqlite")
    }
}

var managedObjectContext: NSManagedObjectContext {
    return persistentContainer.viewContext
}

var workingContext: NSManagedObjectContext {
    let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    context.parent = managedObjectContext
    return context
}

var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "Countdowns")
    
    let storeURL = URL.storeURL(for: "group.app-group-countdowns", databaseName: "Countdowns")
    let description = NSPersistentStoreDescription(url: storeURL)
    
    
    container.loadPersistentStores(completionHandler: { storeDescription, error in
        if let error = error as NSError? {
            print(error)
        }
    })
        
    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
    
    return container
}()

// MARK: For Widget

struct Provider: TimelineProvider {
    var moc = managedObjectContext
    
    init(context : NSManagedObjectContext) {
        self.moc = context
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        return SimpleEntry(date: Date())
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        return completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
}


struct CountdownsWidgetEntryView : View {
    var entry: Provider.Entry
    
    @FetchRequest(entity: Countdown.entity(), sortDescriptors: []) var countdowns: FetchedResults<Countdown>
    
    var body: some View {
        return (
            VStack {
                ForEach(countdowns, id: \.self) { (memoryItem: Countdown) in
                    Text(memoryItem.title ?? "Default title")
                }.environment(\.managedObjectContext, managedObjectContext)
                Text(entry.date, style: .time)
            }
        )
    }
}

@main
struct CountdownsWidget: Widget {
    let kind: String = "CountdownsWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider(context: managedObjectContext)) { entry in
            CountdownsWidgetEntryView(entry: entry)
                .environment(\.managedObjectContext, managedObjectContext)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

struct CountdownsWidget_Previews: PreviewProvider {
    static var previews: some View {
        CountdownsWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

Но у меня проблема: допустим, у меня есть 3 Countdown записи в основном приложении:

В начале просмотра виджета, как и ожидалось, в предварительном просмотре отображаются 3 записи (пользовательский интерфейс для добавления виджета). Но после того, как я добавляю виджет на главный экран, он не показывает Countdown строк, только entry.date, style: .time. При изменении записи на временной шкале строки также не отображаются. Я сделал картинку, чтобы лучше проиллюстрировать это:

добавление виджета

Or:

В стартовом представлении виджета, как и ожидалось, отображаются 3 записи, но примерно через минуту, если я удалю или добавлю Countdown записей в основном приложении, виджет все еще показывает 3 начальных значения, но я хочу, чтобы он отображался фактическое количество значений (для отражения изменений). Временная шкала entry.date, style .time изменяется, отражается в виджете, но не в записях запроса.

Есть ли способ убедиться, что мой виджет показывает правильные результаты запроса на выборку? Спасибо.


person Igor R.    schedule 21.09.2020    source источник
comment
Это может вам помочь: Как обновить данные виджета? - можно применить ту же логику к вашему случаю.   -  person pawello2222    schedule 21.09.2020
comment
@ pawello2222 спасибо за предложение, я просто пробовал аналогичный способ - GitHub Gist, но нет удачи, я получаю неверное значение от func getCountdownsCount() со временем. Например: в первый раз правильно, но после удаления строк в основном приложении - не обновляется count.   -  person Igor R.    schedule 21.09.2020
comment
@ pawello2222 Я просто хотел бы вызвать getCountdownsCount() каждый раз, когда представление перерисовывается (когда отображается новая запись на временной шкале).   -  person Igor R.    schedule 21.09.2020
comment
@ pawello2222 Я чувствую себя очень близко, но что-то не так с контекстом управляемого объекта ...   -  person Igor R.    schedule 21.09.2020


Ответы (1)


Представления виджетов ничего не наблюдают. Им просто предоставлены TimelineEntry данные. Значит, @FetchRequest, @ObservedObject и т. Д. Здесь работать не будут.


  1. Включите удаленные уведомления для вашего контейнера:
let container = NSPersistentContainer(name: "DataModel")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
  1. Обновите свой CoreDataManager, чтобы отслеживать удаленные уведомления:
class CoreDataManager {
    var itemCount: Int?

    private var observers = [NSObjectProtocol]()

    init() {
        fetchData()
        observers.append(
            NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: nil, queue: .main) { _ in
                // make sure you don't call this too often - notifications may be posted in very short time frames
                self.fetchData()
            }
        )
    }

    deinit {
        observers.forEach(NotificationCenter.default.removeObserver)
    }

    func fetchData() {
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")

        do {
            self.itemCount = try CoreDataStack.shared.managedObjectContext.count(for: fetchRequest)
            WidgetCenter.shared.reloadAllTimelines()
        } catch {
            print("Failed to fetch: \(error)")
        }
    }
}
  1. Добавьте еще одно поле в Entry:
struct SimpleEntry: TimelineEntry {
    let date: Date
    let itemCount: Int?
}
  1. Используйте все это в Provider:
struct Provider: TimelineProvider {
    let coreDataManager = CoreDataManager()

    ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let entries = [
            SimpleEntry(date: Date(), itemCount: coreDataManager.itemCount),
        ]

        let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
    }
}
  1. Теперь вы можете отобразить свою запись в представлении:
struct WidgetExtEntryView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(entry.date, style: .time)
            Text("Count: \(String(describing: entry.itemCount))")
        }
    }
}
person pawello2222    schedule 21.09.2020