How can I keep the existing entry on WidgetKit refresh?
Asked Answered
F

1

8

I'm using HealthKit data in my widget. If the phone is locked, it's not possible to get HealthKit data, only if the phone is unlocked. However, my widget timeline's will try to update even if the phone is locked.

Is it possible to return an empty completion somehow, so it will keep the current widget data untouched?

This is my code:

struct Provider: IntentTimelineProvider {
    private let healthKitService = HealthKitService()
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let currentDate = Date()
        let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
        
        healthKitService.getHeartRate() { data, error in
            
            //Create an empty entry
            var entry = SimpleEntry(date: currentDate, configuration: ConfigurationIntent(), data: nil)

            //If no errors, set data
            if(error == nil) {
                entry.data = data
            } else {
                print(error) //this runs when a locked phone does the widget update
            }
            
            //Return response
            let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
            completion(timeline)
        }
    }
}

What I can do is to store the entry data in UserDefaults and load that up in the error route? I'm not sure if thats a good solution though.

Fractious answered 31/12, 2020 at 15:49 Comment(2)
did you find any solution to solve this issue?Herbertherbicide
In the end i just stored the widget data in UserDefaults and if there was nil returned by the healthkit request, i used the stored data instead.Fractious
M
4

The main issue is that you don't have a state in the getTimeline function. This is a similar problem as in How to refresh multiple timers in widget iOS14? - you need some way to store information outside getTimeline.

As you've already mentioned, a possible solution is storing the last entry in the UserDefatuls.

However, you can also try creating your own EntryCache:

class EntryCache {
    var previousEntry: SimpleEntry?
}
struct SimpleEntry: TimelineEntry {
    let date: Date
    var previousDate: Date?
}
struct IntentProvider: IntentTimelineProvider {
    private let entryCache = EntryCache()
    
    // ...

    // in this example I'm testing if the `previousDate` is loaded correctly from the cache
    func getTimeline(for configuration: TestIntentIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
        let currentDate = Date()
        let entry = SimpleEntry(date: currentDate, previousDate: entryCache.previousEntry?.date)
        
        let refreshDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)!
        let refreshEntry = SimpleEntry(date: refreshDate, previousDate: entryCache.previousEntry?.date)

        let timeline = Timeline(entries: [entry, refreshEntry], policy: .atEnd)

        // store the `entry` in the `entryCache`
        entryCache.previousEntry = entry
        completion(timeline)
    }
}

Note

I didn't find any information as to when the TimelineProvider may be re-created. In my tests the Widget was using the same Provider for every refresh but it's safer to assume that the Provider might be re-initialised at one some point in the future. Then, theoretically, for one refresh cycle the previousEntry will be nil.

Mccrary answered 17/1, 2021 at 19:13 Comment(1)
This worked great for me – thank you! One issue I found for my use case was that the cache is shared across all widgets, so if a user has multiple widgets from your app with different configuration, the cache will 'bleed' across the widgets. I solved this by making the cache take configuration into account. Here's a gist that demonstrates it, with the caching abstracted into a protocol so it can be used across multiple types of widget: gist.github.com/shaundon/b4b823fbcac863d24c1ebe751cc97cfcMaciemaciel

© 2022 - 2024 — McMap. All rights reserved.