Recently, I wanted to add a widget to my app that fetches some data from Core Data in the main app. Based on my search, I wrote the following code, but I encountered the following three problems:
The first problem is that I added and modified data in the app, but the widget did not update.
The second problem is that when I run the widget directly on my phone, it displays blank with no error reported.
The third problem is that the original data disappears.
How can I solve these problems? Below is the code used to update the widget:
@main
struct MyMainApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
MyMainAppTabView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
WidgetCenter.shared.reloadAllTimelines()
}
}
}
}
}
The following is my PersistenceController, mainly intended to migrate Core Data to app groups.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Database")
let storeURL = URL.storeURL(for: "group.main.data", databaseName: "Database")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.MyMainApp")
container.persistentStoreDescriptions = [storeDescription]
}
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { [unowned container] (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, withType: NSSQLiteStoreType)
} catch {
print("migration store failed: \(error.localizedDescription)")
}
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url,options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print("delete old store failed: \(error.localizedDescription)")
}
})
}
}
})
}
}
The following is the code related to the widget:
struct Provider: TimelineProvider {
let moc = PersistenceController.shared.container.viewContext
func placeholder(in context: Context) -> SimpleEntry {
return SimpleEntry(date: Date(), items: [])
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), items: [])
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entity = SimpleEntry(date: Date(), items: [])
do {
let result = try moc.fetch(Item.fetchNearExpirationItems())
entity = SimpleEntry(date: Date(), items: result)
} catch {
print("fetch near Expiration items failed: \(error.localizedDescription)")
}
let timeline = Timeline(entries: [entity], policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let items: [Item]
}
struct NearExpirationEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
HStack(spacing: .zero) {
Icon(systemName: "hourglass.tophalf.filled",size: .small)
.foregroundColor(Color("primary"))
Text("Expiration")
Spacer(minLength: .zero)
Text("\(entry.items.count)")
.foregroundColor(Color("onPrimary"))
.padding(UIConstants.padding4)
.background(ContainerRelativeShape().fill(Color("primary")))
}
.font(UIConstants.font13Medium)
ForEach(entry.items){ item in
ItemCellView(name: item.name, count: item.count ?? 0)
}
Spacer(minLength: .zero)
}
.padding(.all)
}
}
struct NearExpiration: Widget {
let kind: String = "NearExpiration"
let contenxt = PersistenceController.shared.container.viewContext
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
NearExpirationEntryView(entry: entry)
.environment(\.managedObjectContext, contenxt)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}