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
Related
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.
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?
This question already has an answer here:
SwiftUI iOS 14 Widget CountDown
(1 answer)
Closed 2 years ago.
So I am new to WidgetKit and SwiftUI, but is there an event or a way to detect when the countdown reaches 00:00 for Text()?
let components = DateComponents(minute: 15)
let futureDate = Calendar.current.date(byAdding: components, to: Date())!
Text(futureDate, style: .timer)
It appears that it is either poorly documented -to me at least- or there is still nothing like that..?
It's not possible, when it reaches 0 it will start counting up.
You have to use the timeline to refresh that widget at the end date of your countdown, using a normal Text that just shows 0:00:00
an example:
var entries: [SingleEntry] = []
// First entry at Date() which is now... with the countdown endDate at 60 seconds in the future
// which you'll use in the Text(date, style)
entries.append(
SingleEntry(
date: Date(),
configuration: configuration,
endDate: Date().addingTimeInterval(60)
)
)
// Second entry which will be scheduled at the Date when you want to stop the timer
// in the TimelineEntry now you can check if endDate is nil and use a normale text
// to say that the countdown is over
entries.append(
SingleEntry(
date: Date().addingTimeInterval(60),
configuration: configuration,
endDate: nil
)
)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
I need to plot graph for steps taken by user on hourly basis on any specific date. But if the user's steps start today at 3:58 pm and end today at 4:10 pm then I am getting just one HKStatistics object for this period of time. I am not able to break this data into two samples as I need to get steps taken in the 3-4 pm slot and the 4-5 pm slot.
static func getSteps(date: Date, duration: DateComponents, completion: #escaping ([HKSample]) -> Void) {
let quantityType : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!]
let stepsQuantityType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let startOfDay = Calendar.current.startOfDay(for: date)
if let endOfDay = Calendar.current.date(byAdding: duration, to: startOfDay) {
var interval = DateComponents()
interval.hour = 1
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: endOfDay, options: .strictStartDate)
let query = HKSampleQuery.init(sampleType:stepsQuantityType,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil,
resultsHandler: { (query, results, error) in
guard let result = results else {
return
}
// print("result healthkit",result.description)
//print("Total count:",)
completion(result)
})
healthStore.execute(query)
}
}
Don't use HKSampleQuery for charting quantity types. HKStatisticsCollectionQuery is designed for this purpose and will split samples that fall into separate regions of your chart for you. See the documentation for examples of how to build the query and use its results.
You're correct, you can't split the sample. That's the all the information that's available. Steps are not stored step-by-step; they're aggregated into blocks to reduce power and storage requirements (mostly power; it's easier to accumulate a value in hardware and periodically read it than to query the real time clock every single time a step is detected).
In order to do what you're discussing, you'll need to average the steps over the period. So if there were 100 steps over the period 3:58p to 4:07p, that averages 10 steps/minute, and you would allocate 20 steps to the 3p-4p block and 80 steps to the 4p-5p block. That's the best information you have.
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?