iOS 14 Widget background refresh not happening - how to get diagnostics / refresh budget counter information? - ios

We are trying to update our home widget around 4 to 5 times an hour within a 10 hours period a day after our application reaches a given state (we can not predict when exactly this will happen).
When the application is in the foreground, everything works fine and as expected, is the application in the background though, the widget gets refresh maybe once an hour or not at all (background processing is enabled and working properly) . It feels totally random and currently we simply can't retrace or comprehend what is happening or why it is happening.
This is our TimelineProvider:
struct Provider: TimelineProvider {
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> Void) {
let preferences = UserDefaults.init(suiteName:widgetGroupId)
let title = preferences?.string(forKey: "title")
let entry = SimpleEntry(date: Date(), title: title ?? "")
completion(entry)
}
func placeholder(in context: Context) -> SimpleEntry {
...
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
getSnapshot(in: context) { (entry) in
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
}
}
Our application tries to update the home widget via WidgetCenter.shared.reloadAllTimelines().
I have now the following questions:
How can I understand why the refresh is not working and when it will be possible again? Is there something like a cooldown time?
How can I check the refresh budget counter? Can we obtain it somehow in the code?
How can I reset the budget counter for testing? Shall I recreate the widget? Restart the phone? Reinstall the app?

From my experience, calling reloadAllTimelines() from anywhere but the foreground app seems to be unreliable. Instead of setting your timeline expiration policy to .never, try using .after with a date 15 minutes or so in the future. That should ensure your widget is updated ~4x an hour with the latest data. Hopefully that’s close enough for your use case.

Related

Try Data(contentsOf:) throws from iPhone widget when iPhone is locked but otherwise works

My widget properly reads (using a call to Data) from an App Group when the phone is active, but throws when the phone is locked/inactive.
Details. A Weather Event is a 336-byte struct written to and read from a JSON file stored in an App Group. The JSON file is shared between an iPhone app and an iPhone widget.
The iPhone app creates the event. The app writes the event to the App Group from where the iPhone widget reads and displays the event's data. The widget can also update (write to) the weather event. This occurs after checking--a getTimeline(…) entry--for an updated weather forecast. The widget updates AS LONG AS the phone remains active, whether or not the app is running. All good!
However, when the screen locked, the widget fails to read the data containing the JSON information. The error occurs when trying to access the data (which occurs before any attempted forecasts). The following code simplifies my code while also failing as described.
func getTimeline(in context: Context, completion: #escaping (Timeline<WeatherEntry>) -> ()) {
Task {
var widgetEvent: Event = Event()
do {
// The "try Data" call below throws only when the iPhone is locked
let data = try Data(contentsOf: FileManager.appEventShare) // <- Throws!
let widgetEvents: [Event] = try JSONDecoder().decode([Event].self, from: data)
if !widgetEvents.isEmpty {
widgetEvent = widgetEvents[0]
}
else {
widgetEvent.userData.name = "No Event"
}
} catch {
print("Widget could not open event", error.localizedDescription)
widgetEvent.userData.name = "Failed Decode"
}
let currentDate = Date.now
let timelineTime = timeForFiveMinuteInterval(from: currentDate)
let entries = [
WeatherEntry(date: currentDate, event: widgetEvent),
WeatherEntry(date: timelineTime, event: widgetEvent)
]
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
Thoughts on what's happening?

Updating a WidgetKit widget from a Siri intents extension

I have an interesting quandary I've been grappling with for the past few days: I have an app that also has a widget extension and a Siri intents extension. There is a button in the app that updates a shared data file. Siri intents extension also updates that data file and the widget reflects those changes (to avoid conflicts, I'm using the NSFileCoordinator API).
When I switch to the app, tap the button, and return to the springboard, the widget contents are updated. When I invoke the intent through a shortcut, the app is updated. However, when I invoke the intent, the widget is not updated.
An interesting wrinkle is that widget updates if the app is run in the debug mode from Xcode, which leads me to suspect that this is some sort of a timing issue.
The app, the widget and the intent extension use shared code to read from and to write to the shared data file.
Here is some code:
A. Button in the app:
// …
Button("Update") {
storage.write(updatedData) { (error) in
guard error == nil else { return }
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
}
}
// …
B. Code in the intents extension
// …
storage.write(updatedData) { (error) in
guard error == nil else {
completion(MyIntentResponse(code: .failure, userActivity: nil))
return
}
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
completion(MyIntentResponse(code: .success, userActivity: nil))
}
// …
As you can see, I handle the situation when writing the data fails in the intents extesion. If that happens, Siri informs me about it – this is how I know that writing succeeds.
C. Widget entry/timeline code
// …
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (MyEntry) -> ()) {
storage.read { (data, error) in
completion(makeEntryFromData(data))
}
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
storage.read { (data, error) in
let timeline = Timeline(entries: [makeEntryFromData(data)], policy: .never)
completion(timeline)
}
}
// …
Here, although the error state seems not to be handled, in actuality, "data" becomes nil when there is an error, and nil values are handled in the view part of the widget – that way I know that data corruption is not the issue either.
Please guide me, as I'm sorely perplexed!
Cheers,
–Baglan
UPDATE: widget seem to update eventually most of the time. My best guess is, there is some some of a mechanism that throttles updates or schedules them for some time convenient for the OS and that results in unpredictable widget update delays.

timer that works in the background Swift 5 [duplicate]

This question already has answers here:
update label from background timer
(1 answer)
iOS Swift Timer in not firing if the App is in the background
(2 answers)
Closed 3 years ago.
I'd like to create a simple timer app that works the same way of the native timer of iOS.
To start I just write some simple code that print the second starting to 0 to infinite.
The first problem was that if you go to the home screen, the task obviously stops to work
so I easily sorted just checking the box into background mode - Audio Airplay, and Picture in Picture (inside project - targets - Signing and Capabilities)
now my task works fine.. even in the background.. unless you put your app into a real device
in this case when you go into the background it doesn't work
after that I searched online for a solution and what I've learnt that Apple does't allow the apps to work into the background as you pleased and after 180 seconds the system just "kill" the background task. I just wonder how all the timer app in the Appstore works..
An Interesting thing that I've come across was when I watched an Apple developer conference that they talk about this new framework of background that you basically can make your app working in the background for heavy tasks when the iPhone is charging, and not only that you can forecast when the user will use your app and have some background tasks that work in the background in order to prepare the app to be updated. The link is this https://developer.apple.com/videos/play/wwdc2019/707/
after this I've tried different approaches to sort my problem but nothing has worked yet.. I have followed this tutorial which I found interesting https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 but it didn't work for me (maybe because of the version of swift outdated or simply because of me) if you guys have managed to make the timer work in the background in your real device let me know.. I would like to understand it well rather than copy and paste the code.
Happy coding to all
the tutorial code:
class RepeatingTimer {
let timeInterval: TimeInterval
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
timer.setEventHandler {}
timer.cancel()
/*
If the timer is suspended, calling cancel without resuming
triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
*/
resume()
eventHandler = nil
}
func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
}

Apple Watch Complication not updating in background

I have an Apple Watch complication that initializes correctly and show the data I am expecting. However, when the NSDate returned in my getNextRequestedUpdateDateWithHandler method triggers a refresh, the only method in my delegate that gets called again is the getNextRequestedUpdateDateWithHandler method (I set breakpoints at every method to determine this). I would have expected requestedUpdateDidBegin to get called when the requested update date occurs, but that doesn't seem to be the case. Does anyone know what could be causing this? Here is my code:
class ComplicationController: NSObject, CLKComplicationDataSource {
/// Provide the time travel directions your complication supports (forward, backward, both, or none).
func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
handler(.Backward)
}
/// Depending on which time travel directions you support, you will be asked for the start/end dates of your timeline (or both, or neither).
/// The start/end dates will determine at what point during time travel we dim out your data to indicate that the timeline does not continue in this direction.
/// Timeline entries after the timeline end date or before the timeline start date will not be displayed.
func getTimelineStartDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) {
let calendar = NSCalendar.currentCalendar()
let now = NSDate()
var startDate: NSDate? = nil
var interval: NSTimeInterval = 0
calendar.rangeOfUnit(NSCalendarUnit.WeekOfMonth, startDate: &startDate, interval: &interval, forDate: now)
handler(startDate)
}
func getTimelineEndDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) {
handler(NSDate())
}
/// Indicate whether your complication's data should be hidden when the watch is locked.
func getPrivacyBehaviorForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
// Since this is showing health data, we want to secure this when the device is locked.
handler(.HideOnLockScreen)
}
/// Indicate your complication's animation behavior when transitioning between timeline entries.
func getTimelineAnimationBehaviorForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineAnimationBehavior) -> Void) {
handler(.Always)
}
/// Provide the entry that should currently be displayed.
/// If you pass back nil, we will conclude you have no content loaded and will stop talking to you until you next call -reloadTimelineForComplication:.
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void) {
// ... custom entry code
handler(currentTemplate)
}
/// The owning complication will use these methods to extend its timeline backwards or forwards.
/// #param date The date of the first/last entry we already have. Return the batch of entries before/after this date.
/// #param limit Maximum number of entries to return.
func getTimelineEntriesForComplication(complication: CLKComplication, beforeDate date: NSDate, limit: Int, withHandler handler: ([CLKComplicationTimelineEntry]?) -> Void) {
//... custom entry code
handler(templates)
}
func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: ([CLKComplicationTimelineEntry]?) -> Void) {
handler([CLKComplicationTimelineEntry]())
}
/// Return the date when you would next like to be given the opportunity to update your complication content.
/// We will make an effort to launch you at or around that date, subject to power and budget limitations.
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
// Refresh in 30 minutes
let refreshDate = NSDate().dateByAddingTimeInterval(60*30)
handler(refreshDate)
}
/// This method will be called when you are woken due to a requested update. If your complication data has changed you can
/// then call -reloadTimelineForComplication: or -extendTimelineForComplication: to trigger an update.
func requestedUpdateDidBegin() {
let complicationServer = CLKComplicationServer.sharedInstance()
for complication in complicationServer.activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
/// This method will be called when we would normally wake you for a requested update but you are out of budget. You can can
/// trigger one more update at this point (by calling -reloadTimelineForComplication: or -extendTimelineForComplication:) but
/// this will be the last time you will be woken until your budget is replenished.
func requestedUpdateBudgetExhausted() {
let complicationServer = CLKComplicationServer.sharedInstance()
for complication in complicationServer.activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
/// When your extension is installed, this method will be called once per supported complication, and the results will be cached.
/// If you pass back nil, we will use the default placeholder template (which is a combination of your icon and app name).
func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) {
//... custom template code
handler(template)
}
}
The endDate is the latest date for which your complication data source is prepared to supply data. If your end date has passed, this implies two things:
The latest timeline entry would be dimmed, as the end of the timeline has been reached.
The complication server would realize that it would be pointless to ask for entries past your endDate as no future entries could possibly exist. In that case, the complication server will only ask for a new update date, as you notice.
The complication server uses a 72-hour sliding window, to ensure 24 hours of time travel in either direction.
Even though you do not support forward time travel, you should ensure a future endDate for a couple of reasons.
If you never want your current entry to be dimmed, the end date must not be reached before the timeline is reloaded.
The complication server must know that your data source can be asked for additional entries, since your timeline is still current.
Each time the timeline is reloaded, it will ask for a new start and end date. Your end date never has to be in the distant future since at best it will be updated every 30 minutes, but it should still be a day in the future, in case your daily budget is exhausted due to frequent updates.
As an aside, the complication server does factor in the time travel boundary dates to determine whether an entry should be added to the timeline.
earliestTimeTravelDate
When constructing your timeline, do not create any entries before this date. Doing so is a waste of time because those entries will never be displayed.
If you're providing entries that go back to the start of a week, you may want to avoid creating entries outside the time travel boundary, as to not exhaust your budget.

Today extension: syncing data with container app

Context
I've been playing around with Today Extensions using this example project.
The app is quite simple:
In the containing app, you have a list of todo items, which you can mark completed
In the Today widget, you see the same list, but you can switch between completed, and incomplete items using a segmented control.
My goal is the following: whenever there is a data change, either in the container app, or the widget, I want both to reflect the changes:
If I mark an item as completed in the container app, then pull down the Notification Center, the widget should be updated
When I do the same in the widget, then return to the app, the app's state should be updated
The implementation
I understand, that the container app, and the extension run in their separate processes, which means two constraints:
NSUserDefaultsDidChangeNotification is useless.
Managing the model instances in memory is useless.
I also know, that in order to access a shared container, both targets must opt-in to the App Groups entitlements under the same group Id.
The data access is managed by an embedded framework, TodoKit. Instead of keeping properties in memory, it goes straight to NSUserDefaults for the appropriate values:
public struct ShoppingItemStore: ShoppingStoreType {
private let defaultItems = [
ShoppingItem(name: "Coffee"),
ShoppingItem(name: "Banana"),
]
private let defaults = NSUserDefaults(suiteName: appGroupId)
public init() {}
public func items() -> [ShoppingItem] {
if let loaded = loadItems() {
return loaded
} else {
return defaultItems
}
}
public func toggleItem(item: ShoppingItem) {
let initial = items()
let updated = initial.map { original -> ShoppingItem in
return original == item ?
ShoppingItem(name: original.name, status: !original.status) : original
}
saveItems(updated)
}
private func saveItems(items: [ShoppingItem]) {
let boxedItems = items.map { item -> [String : Bool] in
return [item.name : item.status]
}
defaults?.setValue(boxedItems, forKey: savedDataKey)
defaults?.synchronize()
}
private func loadItems() -> [ShoppingItem]? {
if let loaded = defaults?.valueForKey(savedDataKey) as? [[String : Bool]] {
let unboxed = loaded.map { dict -> ShoppingItem in
return ShoppingItem(name: dict.keys.first!, status: dict.values.first!)
}
return unboxed
}
return nil
}
}
The problem
Here's what works:
When I modify the list in my main app, then stop the simulator, and then launch the Today target from Xcode, it reflects the correct state. This is true vice-versa.
This verifies, that my app group is set up correctly.
However, when I change something in the main app, then pull down the Notification Center, it is completely out of sync. And this is the part, which I don't understand.
My views get their data straight from the shared container. Whenever a change happens, I immediately update the data in the shared container.
What am I missing? How can I sync up these two properly? My data access class is not managint any state, yet I don't understand why it doesn't behave correctly.
Additional info
I know about MMWormhole. Unfortunately this is not an option for me, since I need to reach proper functionality without including any third party solutions.
This terrific article, covers the topic, and it might be possible, that I need to employ NSFilePresenter, although it seems cumbersome, and I don't completely understand the mechanism yet. I really hope, there is an easier solution, than this one.
Well, I have learned two things here:
First of all, Always double check your entitlements, mine somehow got messed up, and that's why the shared container behaved so awkwardly.
Second:
Although viewWillAppear(_:) is not called, when you dismiss the notification center, it's still possible to trigger an update from your app delegate:
func applicationDidBecomeActive(application: UIApplication) {
NSNotificationCenter.defaultCenter().postNotificationName(updateDataNotification, object: nil)
}
Then in your view controller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserverForName(updateDataNotification, object: nil, queue: NSOperationQueue.mainQueue()) { (_) -> Void in
self.tableView.reloadData()
}
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Updating your Today widget is simple: each time the notification center is pulled down, viewWillAppear(:_) is called, so you can query for new data there.
I'll update the example project on GitHub shortly.

Resources