2

I’m working with two iOS widgets (WidgetA and WidgetB) that both display the value of a flag stored in SwiftData. Each widget can update the flag by triggering an AppIntent when a button is pressed.

After one widget updates the flag, I want the other widget to refresh its timeline so that it shows the updated value. I tried invoking both WidgetCenter.shared.reloadAllTimelines() and WidgetCenter.shared.reloadTimelines(ofKind:) from the AppIntent but while this works fine on the simulator and on a physical device while debugging, it does not work on a physical device outside of debug mode.

Here's my app intent that should trigger the update.

struct ToggleFlagIntent: AppIntent {
    func perform() async throws -> some IntentResult {
        
        // await swiftdata update 

        // Tried any variation of reload timeliness
        WidgetCenter.shared.reloadAllTimelines()
        WidgetCenter.shared.reloadTimelines(ofKind: "WidgetB")
        WidgetCenter.shared.reloadTimelines(ofKind: "WidgetA")

        return .result()
    }
}

The widget that triggers the update (and any of its variations like small/medium/large) gets updated as it is guaranteed by the system as described in WWDC example but other widgets, with a separate definitions, that depend on the same data, do not reload their timeliness hence displaying outdated data.

Did anyone encounter such an issue? Is it at all possible to reload the timelines of other widgets from an AppIntent?

This is a simplified version of my flow, so using a single widget to mimic multiple widget types is not a solution for me.

Thank you!

1 Answer 1

2
+150

I was able to reproduce the issue with UserDefaults, that means the bug is related to the widget's refresh mechanism.

Demo

I suspect it was related to widget's refresh budget, here is the documentation from Apple:


A widget’s budget applies to a 24-hour period. WidgetKit tunes the 24-hour window to the user’s daily usage pattern, which means the daily budget doesn’t necessarily reset at exactly midnight. For a widget the user frequently views, a daily budget typically includes from 40 to 70 refreshes. This rate roughly translates to widget reloads every 15 to 60 minutes, but it’s common for these intervals to vary due to the many factors involved.

Note

The system takes a few days to learn the user’s behavior. During this learning period, your widget may receive more reloads than normal. Cases in which WidgetKit doesn’t count reloads against your widget’s budget include when:

  • The widget’s containing app is in the foreground.
  • The widget’s containing app has an active audio or navigation session. The widget performs an app intent, such as when the user taps a button or toggles a switch.
  • The widget performs an animation.
  • The system locale changes.
  • Dynamic Type or Accessibility settings change.
  • For cases such as system appearance changes or system locale changes, don’t request a timeline reload from your app. The system updates your widgets automatically. In StandBy, the system refreshes your widget’s display at a system-defined rate that doesn’t count against the its budget.
  • WidgetKit does not impose this limit when debugging your widget in Xcode. To verify that your widget behaves correctly, test your app and widget’s behavior outside of Xcode’s debugger.

Among which, we can see apple made an exception to this:

The widget performs an app intent, such as when the user taps a button or toggles a switch.

That means we can easily update our own widget without worrying about the budget. However, if we want to refresh other widgets using WidgetCenter.shared.reloadAllTimelines(), the widget refresh will be subject to budget.

If you observe the console log, you will see clear evidence:

Task [2318] [com.cleverlearn.Trask::com.cleverlearn.Trask.TraskWidgets:TodoListWidget:-2125748069729905058] Ignoring low-priority task with configuration: [externalRequest([source:<BSProcessHandle: 0x69c7897a0; TraskWidgets:23827; valid: YES>], reason: WidgetCenterServer)-immediate-budgeted-1]; Current: task [[2226-externalRequest([source:<BSProcessHandle: 0x69b8957a0; TraskWidgets:23522; valid: NO>], reason: WidgetCenterServer)-immediate]], Queued: task identifier: 2231; lifetime: 1637891ms; configuration: [externalRequest([source:<BSProcessHandle: 0x69b406850; TraskWidgets:23522; valid: NO>], reason: WidgetCenterServer)-immediate-budgeted-1]

The console log reason: WidgetCenterServer)-immediate-budgeted-1 confirms that:

When you call reloadAllTimelines() or reloadTimelines(ofKind:) from within an intent, those other widgets are still subject to the budget, which is why TodoListWidget doesn't update.

PS: I tested and waited enough long time, and the target widget finally catch up with the correct timeline, you can even choose to change device locale to let it refresh immediately.

PS: Tapping on Widget A and another instance of Widget A will not subject to the refresh budget, you already mentioned that.


If someone want to seek a hacky solution, this is the simple project to begin with.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for your detailed answer. I do indeed see the reason: WidgetCenterServer)-immediate-budgeted-1 log when trying to reload the timelines of my other widget. It is strange to me that the reload does not happen at least once, immediately. That was why I did not consider the budgeting at first. I can see that it's not 60 times a day and then no reloads are available but its 60 times a day dispersed in time intervals, so I can't have for example 2 reloads back to back in the same minute.
Yes, that's right. We can always validate your code's correctness by changing device's language, for example setting device language from English to France. That will 100% reload springboard(system UI), and hence reset widget's budgets. In a word, that's just system's limitations.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.