iOS widgets auto-refresh - ios

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.

Related

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

Timeline reload policy seems to be ignored in WidgetKit

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?

Detect when Text.DateStyle.timer reaches 00:00 [duplicate]

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)

Local notification cancel for particular time period

I want to fire local notification on a particular time based just liked water notification.
Like user can set wake up time & go to bed time n drink water notification for every 2 hours so how can i set that between goto bed to wake up time my local notification won;t fire, other than that the notification will fire for every 2 hours..
Please help me.
Thanks
What you can do is to obtain the hours in between the wake up time and sleep time with the frequency of what you wish your notification to fire off.
Firstly, you would need to function to create a local notification that repeats during a particular hour everyday.
func createNotification(title: String, body: String, hour: Int, repeats: Bool, identifier: String) {
//calendar
let calendar = Calendar.current
let content = UNMutableNotificationContent()
content.title = title
content.body = body
let today = Date()
let futureDate = calendar.date(byAdding: .hour, value: hour, to: today)
let hourComponent = calendar.dateComponents([.hour], from: futureDate!)
let trigger = UNCalendarNotificationTrigger(dateMatching: hourComponent, repeats: repeats)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
center.add(request, withCompletionHandler: {(error) in })
}
Secondly, place this code in your viewDidLoad() or the place you wish to set the notifications. What I am doing here is obtaining the wakeup hour and sleep hour and then finding the number of notifications that has to be made using the frequency that I want by dividing the difference in hour with the frequency.
I end of by creating a for loop that creates the notification based on the time that I add to the wakeup hour. eg. First notification fires of at 6am + 1x2 = 8am, second notification fires of at 6am + 2x2 = 10am
//calendar
let calendar = Calendar.current
//set the wakeup and sleep hour
let wakeupHour = 6 //6am
let sleepHour = 18 // 6pm
//Obtain starting time using date components
let wakeup = DateComponents(calendar: calendar, hour: wakeupHour)
//Obtain ending time using date components
let sleep = DateComponents(calendar: calendar, hour: sleepHour)
//calculate the number of hours between the two time
//obtain number of hour difference
let hourDifference = calendar.dateComponents([.hour], from: wakeup, to: sleep)
//how many hours do you want
let frequency:Int = 2
//how many notifications will there be between the wakeup and sleeptime
let numberOfNotifications:Int = hourDifference.hour! / frequency
//create the notifications
for num in 1...numberOfNotifications {
//hour that each notification will fire off
let hour = wakeupHour + frequency * num
createNotification(title: "Drink Up", body: "Drink Up", hour: hour, repeats: true, identifier: "drink\(hour)")
}
I'm still learning swift so this may not be the most efficient way of solving this problem but I hope this helps!

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