问题
I have an app that uses Core Data with CloudKit. Changes are synced between devices. The main target has Background Modes capability with checked Remote notifications. Main target and widget target both have the same App Group, and both have iCloud capability with Services set to CloudKit and same container in Containers checked.
My goal is to display actual Core Data entries in SwiftUI WidgetKit view.
My widget target file:
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))
}
}
But I have a problem: let's say I have 3 Countdown
records in the main app:
At the start widget view shows 3 records as expected in preview (UI for adding a widget). But after I add a widget to the home screen, it does not show Countdown
rows, only entry.date, style: .time
. When timeline entry changes, rows not visible, too. I made a picture to illustrate this better:
Or:
At the start widget view shows 3 records as expected, but after a minute or so, if I delete or add Countdown
records in the main app, widget still shows initial 3 values, but I want it to show the actual number of values (to reflect changes). Timeline entry.date, style .time
changes, reflected in the widget, but not entries from request.
Is there any way to ensure my widget shows correct fetch request results? Thanks.
回答1:
Widget views don't observe anything. They're just provided with TimelineEntry
data. Which means @FetchRequest
, @ObservedObject
etc. will not work here.
- Enable remote notifications for your container:
let container = NSPersistentContainer(name: "DataModel")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
- Update your CoreDataManager to observe remote notifications:
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)")
}
}
}
- Add another field in the
Entry
:
struct SimpleEntry: TimelineEntry {
let date: Date
let itemCount: Int?
}
- Use it all in the
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)
}
}
- Now you can display your entry in the view:
struct WidgetExtEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text("Count: \(String(describing: entry.itemCount))")
}
}
}
来源:https://stackoverflow.com/questions/63991081/how-to-ensure-widgetkit-view-shows-correct-results-from-fetchrequest