Timeline reload policy seems to be ignored in WidgetKit - ios

In the following example, I create 4 timeline entries in one-second intervals, specifying the timeline reload policy .atEnd.
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
var entries: [SimpleEntry] = []
for secondOffset in 0...3 {
let entryDate = Calendar.current.date(byAdding: .second, value: secondOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
completion(Timeline(entries: entries, policy: .atEnd))
}
The widget view will show the second of the timeline entry's date, just to make it visible to the user that the widget has been updated.
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("\(Calendar.current.component(.second, from: entry.date))")
}
}
}
Whenever I run this on the simulator or an actual iPhone, the widget will count up 4 times, e.g. showing "17", "18", "19", "20". After that, it'll stop updating.
I expected WidgetKit to request a new timeline at this point, since the timeline reload policy .atEnd has been specified.
I'm aware that in this case the date of the last entry only signifies the earliest date for a new timeline to be requested, but it seems that a new timeline will never be requested, even after minutes of waiting or locking/unlocking the phone etc.
I have found this possibly related question, but my example seems to be even simpler, so I think it might be worth asking.
Am I misunderstanding how the timeline policy works?

Related

iOS widgets auto-refresh

For testing purposes, I'm trying to refresh a widget every 30 sec with the following code.
// PWContent is the TimelineEntry object.
func getTimeline(in context: Context, completion: #escaping (Timeline<PWContent>) -> ()) {
NSLog("PWTimelineProvider.getTimeline(in)")
let currentDate: Date = Date.now
let calendar: Calendar = Calendar.current
// Display a random element from the array
let entry: PWContent = PWContent(date: currentDate, planet: PWCustomData.sPlanets.randomElement()!)
NSLog("Current time = " + String(describing: currentDate))
// Set refresh date to 30sec in the future.
let refreshDate = calendar.date(byAdding: .second, value: 30, to: currentDate)!
NSLog("Refresh time = " + String(describing: refreshDate))
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
But I get the following logs
PWTimelineProvider.getTimeline(in)
Current time = 2023-01-08 07:53:31 +0000
Refresh time = 2023-01-08 07:54:01 +0000
PWTimelineProvider.getTimeline(in)
Current time = 2023-01-08 07:58:31 +0000
Refresh time = 2023-01-08 07:59:01 +0000
PWTimelineProvider.getTimeline(in)
Current time = 2023-01-08 08:03:31 +0000
Refresh time = 2023-01-08 08:04:01 +0000
As shown in the above logs, the widget refreshes every 5 mins (The difference b/w two 'Current Time'). When checking the Widget in home screen, it doesn't update every 30 sec.
My understanding (which is not what's observed): Widget first updates itself at 7:53:31. When the time is 7:54:01, getTimeline function is invoked (since the refresh policy was set to 7:54:01 i.e. 30 sec later than the previous time) by iOS to request another timeline, which again provides only one entry at the current time (= 7:54:01) and the new refresh policy is set to 7:54:31, which is when getTimeline is invoked again to get the next timeline.
Reference: Provide Timeline Entries section in this wiki.
According to documentation,
WidgetKit imposes a minimum amount of time before it reloads a widget. Your timeline provider should create timeline entries that are at least about 5 minutes apart.
And that's why, despite setting up a refresh period of 30sec, it still refreshes after 5mins.
But, in the following getTimeline(),
// ISWContent is the TimelineEntry object.
func getTimeline(in context: Context, completion: #escaping (Timeline<ISWContent>) -> ()) {
NSLog(ISW_TAG + "ISWTimelineProvider.getTimeline(in)")
let increment: Int = 15
var entries: [ISWContent] = []
let currentDate = Date.now
// Generate a timeline consisting of 4 entries 15 sec apart,
// starting from the current date.
NSLog(ISW_TAG + "Current time = " + String(describing: currentDate))
for index in 0...3 {
let entryDate = Calendar.current.date(byAdding: .second, value: (index * increment), to: currentDate)!
NSLog(ISW_TAG + "incremental dates[%d] = " + String(describing: entryDate), index)
let entry = ISWContent(date: entryDate, state: ISWCustomData.sIndianStates.randomElement()!)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
Widget is refreshed every 15secs and once the last timeline entry has expired, getTimeline is invoked immediately to provide the next timeline. The 5mins restriction doesn't apply here.
Anyway, according to documentation, whatever timeline we return, is only a request and may not be granted if the user rarely views the home screen containing the widget or the app.

Swift Widget: getTimeline with completion handler

I have a iOS Widget that I am trying to update every 5 or 15 minutes.
I am new to widgets and do not understand how to loop the timeline with an async call.
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let price: Double
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
networkManager.fetchData { price in
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration, price: price)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have a completion handler that passes in the price from the async api call.
networkManager.fetchData { price in
}
It looks like your networkManager.fetchData is providing only one price value at a time.
In such cases, when only one entry is provided at a time then the following should be enough:
func getTimeline(for configuration: ConfigurationIntent,
in context: Context,
completion: #escaping (Timeline<Entry>) -> ()) {
networkManager.fetchData { (price) in
let currentDate = Date()
//create the entry for the given price
let entry = SimpleEntry(date: currentDate,
configuration: configuration,
price: price)
/*
If you can predict price values then you need the loop for multiple entries.
However in your case, it seems this one entry is sufficient
*/
let entries = [entry]
//next reload date; 15mins in this case
let reloadDate = Calendar.current.date(byAdding: .minute,
value: 15,
to: currentDate)!
//your timeline with one entry that will refresh after given date
let timeline = Timeline(entries: entries, policy: .after(reloadDate))
completion(timeline)
}
}
By specifying after(_ date:) as the TimelineReloadPolicy, iOS will refresh the widget after the given date.
The timeline’s refresh policy specifies the earliest date for WidgetKit to request a new timeline from the provider. The default refresh policy, .atEnd, tells WidgetKit to request a new timeline after the last date in the array of timeline entries you provide. However, you can use .afterDate to indicate a different date either earlier or later than the default date. Specify an earlier date if you know there’s a point in time before the end of your timeline entries that may alter the timeline.
Ref: Apple Documentation on Timeline
This solution was to address the core issue, so finally I would like to add a disclaimer that reloading every 15mins might be overkill. Hence tweak your logic to specify the TimelineReloadPolicy intelligently. If you can predict values & create a timeline of multiple entries with fewer network calls then great! If not then... well... best of luck :)
Important
Plan ahead if your widget makes requests to a server when it reloads, and uses afterDate() with a specific date in timeline entries. WidgetKit tries to respect the date you specify, which may cause a significant increase in server load when multiple devices reload your widget at around the same time.
Ref: Keeping a Widget Up To Date
More Read:
Apple Documentation on TimelineProvider

iOS14.5 Widget data not up-to-date

I use the following code to update my widget's timeline, but the "result" which I fetched from the core data is not up-to-date.
My logic is when detecting the host app goes to background I call "WidgetCenter.shared.reloadAllTimelines()" and fetch the core data in the "getTimeline" function. After printing out the result, it is old data. Also I fetch the data with the same predicate under the .background, the data is up-to-date.
Also I show the date in the widget view body, when I close the host app, the date is refreshing. Means that the upper refreshing logic works fine. But just always get the old data.
Could someone help me out?
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let now = Date()
let newRequest = coreDataManager.fetchRequestForTimeline(date: now)
let result = try? viewContext.fetch(newRequest)
print(result!)
print("new####")
entries.append(SimpleEntry(date: now, tasks: result))
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
This is the code in the host app
case .background:
WidgetCenter.shared.reloadAllTimelines()
// fetch here
print("App is in background")
Update:
I added the following code to refresh the core data before I fetch. Everything work as expect.
// refresh the core data
try? viewContext.setQueryGenerationFrom(.current)
viewContext.refreshAllObjects()
Update:
I added the following code to refresh the core data before I fetch. Everything work as expect.
// refresh the core data
try? viewContext.setQueryGenerationFrom(.current)
viewContext.refreshAllObjects()

Inconsistent Widget Behaviour Between Devices in iOS 14

I recently launched an iOS 14 Widget for my app. In testing everything seemed a-okay but on launch I am having inconsistent behaviour. On some devices the widget loads as just black, on other devices it only loads the placeholder content and on some devices it works as intended. The behaviour exhibited, good or bad, is consistent in the Add Widget Screen and when it's added to the Home Screen. On the devices where my widget does not work, other widgets do work.
The content in the Widget only changes when something is changed in the app thus I call WidgetCenter.shared.reloadAllTimelines() in the SceneDelegate when sceneWillResignActive is called. The expected behaviour is that when the user backgrounds the application the widget will update. On the devices that show black or placeholder content this Widget Center Update does not work, on the devices where it does work the update function works as expected.
This is the code for my Widget:
struct ThisWeekProvider: TimelineProvider {
func placeholder(in context: Context) -> ThisWeekEntry {
return ThisWeekEntry(date: Date(), thisWeekJSON: getDefaultTimeSummaryJSON())
}
func getSnapshot(in context: Context, completion: #escaping (ThisWeekEntry) -> ()) {
var thisWeekData = TimeSummaryJSON()
if context.isPreview {
thisWeekData = getThisWeekData()
} else {
thisWeekData = getThisWeekData()
}
let entry = ThisWeekEntry(date: Date(), thisWeekJSON: thisWeekData)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let entries: [ThisWeekEntry] = [ThisWeekEntry(date: Date(), thisWeekJSON: getThisWeekData())]
let timeline = Timeline(entries: entries, policy: .after(entries[0].thisWeekJSON.endDate))
completion(timeline)
}
}
struct ThisWeekEntry: TimelineEntry {
let date: Date
let thisWeekJSON: TimeSummaryJSON
}
struct ThisWeekWidgetEntryView : View {
var entry: ThisWeekProvider.Entry
var body: some View {
// Generate View
// Use data from 'entry' to fill widget
}
struct ThisWeekWidget: Widget {
let kind: String = K.thisWeekWidget
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ThisWeekProvider()) { entry in
ThisWeekWidgetEntryView(entry: entry)
}
.configurationDisplayName("this_week_widget".localized())
.description("this_week_description".localized())
.supportedFamilies([.systemSmall])
}
}
The custom data type 'TimeSummaryJSON' is as follows:
struct TimeSummaryJSON: Codable {
var labourIncome: String = "$0.00"
var equipmentIncome: String = "$0.00"
var days: String = "0"
var hours: String = "0"
var endDate: Date = Date()
var settingsFirstDayOfWeek: String = K.monday
var localeFirstDayOfWeek: Bool = false
}
The custom function that retrieves the data 'getThisWeekData()' is as follows:
private func getThisWeekData() -> TimeSummaryJSON {
if let encodedData = UserDefaults(suiteName: AppGroup.shared.rawValue)!.object(forKey: K.thisWeek) as? Data {
if let thisWeekJSON = try? JSONDecoder().decode(TimeSummaryJSON.self, from: encodedData) {
return checkExpiryForCurrentWeek(thisWeekJSON)
} else {
print("Decoding Error - Return Default This Week")
return getDefaultTimeSummaryJSON()
}
} else {
print("No Shared Data - Return Default This Week")
return getDefaultTimeSummaryJSON()
}
}
The process of saving and retrieving the data works like this:
SceneDelegate calls sceneWillResignActive
Data is pulled from the Local Realm Database, calculated and saved into a TimeSummaryJSON
TimeSummaryJSON is encoded and saved to a shared AppGroup
WidgetCenter.shared calls reloadAllTimelines()
Widget decodes JSON data from AppGroup
If the JSON Decode is successful the current user data is shown in the widget, if the JSON Decode fails a default TimeSummaryJSON is sent instead
I've looked over my code quite extensively and read countless forums and it seems that I am doing everything correctly. Have I missed something? Could anyone suggest why the behaviour is inconsistent between devices? I'm well and truly stuck and am not sure what to try next.
Any help you can offer would be kindly appreciated.
Thank you!
I finally got to the bottom of my issue thanks to some help on reddit. The problem was an image I was using was too large in resolution, the large file size capped out the 30MB Memory Limit of Widgets.
The reddit user that helped me had this to say:
'Black or redacted widget means that during loading you hit the 30 mb memory limit.
Also if you're reloading more than one widget at a time, it seems that there's a higher chance you'll hit the memory budget.'
'Forgot to mention that black/redacted widget can also happen if the widget crashes (not necessary due to memory budget). So maybe you have some threading problems or are force unwrapping something that is nil.'
This is the link to the full discussion:
https://www.reddit.com/r/iOSProgramming/comments/mmo5lf/inconsistent_widget_behaviour_between_devices_in/

ClockKit CLKComplicationDataSource missing backward events

I write a test app with complications support
For some reason clock faces presenting only 1-2 backward events, but I can see in logs 10-15 events before current date.
And when I return an empty array for forward events all my backward events start showing in clock face.
Here is my function
func getTimelineEntriesForComplication(complication: CLKComplication, beforeDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
var entries: [CLKComplicationTimelineEntry] = []
let events = self.events.filter { (event: CEEvent) -> Bool in
return date.compare(event.startDate) == .OrderedDescending
}
var lastDate = date.midnightDate
for event in events {
let entry = CLKComplicationTimelineEntry(date: lastDate, complicationTemplate: event.getComplicationTemplate(complication.family))
if let endDate = event.endDate {
lastDate = endDate
} else {
lastDate = event.startDate
}
entries.append(entry)
if entries.count >= limit {
break
}
}
handler(entries)
}
P.S. I know about 'limit' parameter and it's always greater than my array's count
P.P.S. Sorry about my English :)
I've seen that identical behavior for watchOS 2.0.1 where time travel backwards initially only shows two earlier entries, even though the datasource was asked for and returned 100 entries.
About 15 minutes after launch, more entries started appearing for backwards time travel. About 30 minutes after launch, all 100 prior entries were present.
This was not due to any update I scheduled, as my complication's update interval is 24 hours.
It appears that the complication server prioritizes adding the forward entries, but defers populating the cache with all the backward time travel entries. You'd have to ask Apple whether it's an optimization or a bug.
I don't know if this is a coincidence, but my timeline entries are spaced 15 minutes apart. Perhaps when the complication server updates the complication to show the new timeline entry, it also adds more of the batched earlier entries?

Resources