如何确保WidgetKit视图显示来自@FetchRequest的正确结果?

问题描述

我有一个将Core Data与CloudKit结合使用的应用程序。更改在设备之间同步。主要目标具有“后台模式”功能以及选中的“远程通知”。主要目标和小部件目标都具有相同的应用程序组,并且都具有iCloud功能,并且服务设置为CloudKit,并且在选中的容器中具有相同的容器。

我的目标是在SwiftUI WidgetKit视图中显示 actual 核心数据条目。

我的小部件目标文件

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条记录(在预览(用于添加窗口小部件的UI)中按预期)。但是,在将小部件添加到主屏幕后,它不显示Countdown行,仅显示entry.date,style: .time行。当时间线条目更改时,行也不可见。我做了一张照片以更好地说明这一点:

adding a widget

或者:

在开始时,小部件视图会按预期显示3条记录,但是大约一分钟后,如果我在主应用程序中删除添加Countdown条记录,小部件 still 显示初始3个值,但我希望它显示值的实际数量(以反映更改)。时间轴entry.date,style .time的变化反映在窗口小部件中,但不反映请求中的条目。

有什么方法可以确保我的小部件显示正确的提取请求结果?谢谢。

解决方法

小部件视图不观察。它们仅提供了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))")
        }
    }
}