Tio
Tio

Reputation: 1036

How to reload fetch data from CoreData in Widget iOS 14 when host App closes

I'm currently developing an application using SwiftUI.

I want to reload data from CoreData in a widget to update newer data every when I close a host App.

But in my codes, the data doesn't get reloaded...

How could I solve that?


TimerApp.swift (Host APP)

import SwiftUI
import WidgetKit

@main
struct TimerApp: App {
    @Environment(\.scenePhase) private var scenePhase
    
    let persistenceController = PersistenceController.shared.managedObjectContext
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController)
                .onChange(of: scenePhase) { newScenePhase in
                    if newScenePhase == .inactive {
                        WidgetCenter.shared.reloadAllTimelines()
                    }
                }
        }
    }
}

TimerWidget.swift(Widget Extension)

import WidgetKit
import SwiftUI
import CoreData

struct Provider: TimelineProvider {
    
    var moc = PersistenceController.shared.managedObjectContext
    var timerEntity:TimerEntity?
    
    init(context : NSManagedObjectContext) {
        self.moc = context
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
        
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        return SimpleEntry(date: Date(), timerEntity: timerEntity!)
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), timerEntity: timerEntity!)
        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, timerEntity: timerEntity!)
            entries.append(entry)
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let timerEntity:TimerEntity
}

struct TimerWidgetEntryView : View {
        
    var entry: Provider.Entry
    
    var body: some View {
        return (
            VStack{
                Text(entry.timerEntity.task ?? "")
            }
        )
    }
}

@main
struct TimerWidget: Widget {
    let kind: String = "TimerWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in
            TimerWidgetEntryView(entry: entry)
                .environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext)
        }
    }
}

UPDATED

I updated my code to execute NSFetchRequest in the getTimeline function as well as below.

But in my code, when I build this project I have an error like below:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TimerEntity task]: unrecognized selector sent to instance 0x600003175400' terminating with uncaught exception of type NSException

struct Provider: TimelineProvider {
    
    var moc = PersistenceController.shared.managedObjectContext
    @State var timerEntity:TimerEntity?
    
    init(context : NSManagedObjectContext) {
        self.moc = context
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
        
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        return SimpleEntry(date: Date(), timerEntity: timerEntity ?? TimerEntity())
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), timerEntity: timerEntity!)
        return completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        let currentDate = Date()
        
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
        
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, timerEntity: timerEntity ?? TimerEntity())
            entries.append(entry)
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Xcode: Version 12.0.1

iOS: 14.0

Life Cycle: SwiftUI App

Upvotes: 1

Views: 1373

Answers (1)

pawello2222
pawello2222

Reputation: 54426

The code responsible for fetching data is only in init:

struct Provider: TimelineProvider {
    var moc = PersistenceController.shared.managedObjectContext
    var timerEntity:TimerEntity?
    
    init(context : NSManagedObjectContext) {
        self.moc = context
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
        
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
    }
    ...
}

It is run only when the Provider is initialised. You need to execute this NSFetchRequest in the getTimeline function as well, so your timerEntity is updated.


Also, if you execute WidgetCenter.shared.reloadAllTimelines() every time scenePhase == .inactive it might be too often and your Widget might stop getting updated.

inactive is called whenever your app is moved from the background to the foreground (in both directions).

Try using background instead - this will be called only when your app is minimised or killed:

.onChange(of: scenePhase) { newScenePhase in
    if newScenePhase == .background {
        WidgetCenter.shared.reloadAllTimelines()
    }
}

Note: I'd recommend adding some safeguards around WidgetCenter.shared.reloadAllTimelines() to make sure to not refresh it too often.

Upvotes: 1

Related Questions