Victor Khodalov
Victor Khodalov

Reputation: 171

WidgetKit doesn't fetch updated data from Core Data when WidgetCenter.shared.reloadAllTimelines() gets called

The app uses Core Data + CloudKit. When the app gets launched WidgetKit fetches correct entries from Core Data but when I add new entries to Core Data from the main app and call WidgetCenter.shared.reloadTimelines() updated entries don't get fetched and old data is displayed inside the widget.

Main app and Widget target have the same App group, iCloud capability enabled for both targets.

When I call WidgetCenter.shared.reloadTimelines() from the Main app Widget reloads (timer added to Widget view sets to zero) but fetch from Core Data doesn't return newly added entries. Then I restart the app and correct entries get fetched.

Why doesn't it fetch correct entries when WidgetCenter.shared.reloadTimelines() gets called and only works when app is relaunched?

Widget's code:

import WidgetKit
import SwiftUI
import CoreData

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> HabitsEntry {
        HabitsEntry(date: Date(), habitsCount: 0)
    }

    func getSnapshot(in context: Context, completion: @escaping (HabitsEntry) -> ()) {
        let entry = HabitsEntry(date: Date(), habitsCount: 0)
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {        
        let managedObjectContext = Storage.viewContext
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: "DailyHabit")
        let predicate = Storage.getCurrentDatePredicate()
        request.predicate = predicate
        var habits = 0
        do { habits = try managedObjectContext.count(for: request)  }
        catch let error as NSError {print ("Could not fetch \(error), \(error.userInfo)")}
        let entry = [HabitsEntry(date: Date(), habitsCount: habits)]
        let timeline = Timeline(entries: entry, policy: .never)
        completion(timeline)
    }
}

struct HabitsEntry: TimelineEntry {
    let date: Date
    var habitsCount: Int
}

struct HabitsHomeWidgetEntryView : View {
    var entry: Provider.Entry
    var body: some View {
      Text(entry.date, style: .timer)
      Text("There are \(entry.habitsCount) daily habits")
    }
}

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

Here is the code for Core Data:

import UIKit
import CoreData
import WidgetKit

class Storage {
  
  static let shared = Storage()
  
  static var persistentContainer: NSPersistentContainer {
    return Storage.shared.persistentContainer
  }
  
  static var viewContext: NSManagedObjectContext {
    return persistentContainer.viewContext
  }
  
  lazy var persistentContainer: NSPersistentContainer = {
    let container: NSPersistentContainer
    
    let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)!
    let storeURL = containerURL.appendingPathComponent("NoRegrets.sqlite")
    let description = NSPersistentStoreDescription(url: storeURL)
    if isICloudContainerAvailable {
      container = NSPersistentCloudKitContainer(name: Constants.persistentContainerName)
    } else {
      container = NSPersistentContainer(name: Constants.persistentContainerName)
    }
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    description.shouldMigrateStoreAutomatically = true
    description.shouldInferMappingModelAutomatically = true

    let storeDescription = description
    
    container.persistentStoreDescriptions = [storeDescription]
    
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        return
      }
    })
    
    container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    container.viewContext.automaticallyMergesChangesFromParent = true
    
    do {
      try container.viewContext.setQueryGenerationFrom(.current)
    } catch {
      print("Failed to pin viewContext to the current generation: \(error)")
    }
    return container
  }()
  
  var isICloudContainerAvailable: Bool {
    FileManager.default.ubiquityIdentityToken != nil
  }
  
  // MARK: - Core Data Saving support
  func saveContext() {
    let context = persistentContainer.viewContext
    if context.hasChanges {
      do {
        try context.save()
        
      } catch {
        let nserror = error as NSError
        print("Saving error: \(nserror)")
      }
    }
  }
}

// MARK: - Helper methods
extension Storage {
  func getCurrentUser() -> User? {
    let fetchRequest = User.createFetchRequest()
    fetchRequest.fetchLimit = 1
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "objectID", ascending: false)]
    
    let user = try? Storage.viewContext.fetch(fetchRequest).first
    return user
  }
  
  class func getCurrentDatePredicate() -> NSPredicate {
    var calendar = Calendar.current
    calendar.timeZone = NSTimeZone.local
    let dateFrom = calendar.startOfDay(for: Date())
    let dateTo = calendar.date(byAdding: .day, value: 1, to: dateFrom)
    let fromPredicate = NSPredicate(format: "date >= %@", dateFrom as NSDate)
    let toPredicate = NSPredicate(format: "date < %@", dateTo! as NSDate)
    return NSCompoundPredicate(andPredicateWithSubpredicates: [fromPredicate, toPredicate])
  }
}

Upvotes: 5

Views: 1147

Answers (1)

Victor Khodalov
Victor Khodalov

Reputation: 171

It turns out all I had to do was update Core Data's context to the current store generation (in WidgetKit file, getTimeline() function) before making a fetch:

try? Storage.viewContext.setQueryGenerationFrom(.current)
Storage.viewContext.refreshAllObjects()

Please read more on this here: https://developer.apple.com/documentation/coredata/accessing_data_when_the_store_has_changed

Upvotes: 8

Related Questions