Keeping our widgets up to date
We have learned that we need to look ahead and create a timeline with different entries and dates to keep our widget up to date. But how does our widget work under the hood?
Widgets don’t get any running time – once we generate the timeline entries, WidgetCenter generates their different views, keeps them persistently, and just switches them according to the provided timeline.
So, there’s no way to update our widget without reloading the timeline, and when we created our timeline, we had to define its reload policy:
let timeline = Timeline(entries: entries, policy: .atEnd)
However, sometimes, we want to instruct WidgetCenter
to reload the timeline immediately, due to data changes or any other alterations.
Let’s see how it happens.
Reload widgets using the WidgetCenter
Throughout the chapter, I have mentioned WidgetCenter frequently but I haven’t explained what it means.
WidgetCenter is an object that holds information about the different configured widgets currently used, and it also provides an option to reload them.
To use WidgetCenter
, we need to call the shared
property to access its singleton reference:
WidgetCenter.shared
The difference between WidgetCenter and the rest of the code we have handled up until now is the fact that we call WidgetCenter from the app and not the widget extension.
Let’s see how we can call the WidgetCenter
to get a list of active widgets:
func getConfigurations() { WidgetCenter.shared.getCurrentConfigurations { result in if let widgets = try? result.get() { // handle our widgets } } }
The getCurrentConfigurations
function uses a closure to return an array of active widgets. Each one of them is the WidgetInfo
type – a structure that contains information about a specific configured widget.
The WidgetInfo
structure has three properties – kind, family, and configuration:
kind
– This is the string we set when we created the widget configuration (look again at the Configuring our widget section).family
– The family size of the widget – small, medium, or large.configuration
– The intent that contains user configuration information. Theconfiguration
property is optional.
If needed, we can use that information to reload the timeline of a specific kind of widget. For example, if we want to reload widgets with the kind of MyWidget
, we need to call the following:
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
Notice that the function says Timelines
and not Timeline
, as it is possible to have several widgets of the same kind.
If we want to reload all our app widgets, we can call the reloadAllTimelines()
function:
WidgetCenter.shared.reloadAllTimelines()
There are several great use cases for reloading our widget timeline, such as when we get a push notification, or when the user data or settings have changed. If you remember, when we discussed the widget timeline in the Generating a timeline section, we talked about the fact that widgets have a certain budget for the amount of reloading they can do each day. But the good news is that calling the reloadTimelines
or reloadAllTimelines
functions doesn’t count in this budget if our app is in the foreground or uses some other technique, such as playing audio in the background.
In most cases, reloadTimelines
works well when the updated data is already on the device or in our app. But what should we do when the local persistent store is not updated?
We perform a network request, of course!
Go to the network for updates
Performing a network request to update local data is a typical operation in mobile apps. But how does it work in widgets?
Let’s look at the getTimeline
function again:
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())
We can see that the getTimeline
function is an asynchronous function. It means that when we build our timeline, we can perform async operations such as open URL sessions and fetching data.
Let’s see an example of requesting the next calendar events:
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) { var entries: [SimpleEntry] = [] calendarService.fetchNextEvents { result in switch result { case .success(let events): for event in events { let entry = SimpleEntry(date: event.alertTime, nextEvent: event.title, nextEventTime: event.date) entries.append(entry) } case .failure(let error): print("Error fetching next events: \(error.localizedDescription)") } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
The getTimeline
function implementation is similar to the previous getTimeline
implementation we saw in the Generating a timeline section, and this time, we are fetching the events using the calendarService
instance. The calendarService
goes to our server and returns an array of events. Afterward, we loop the events, generate timeline entries, and return a timeline using the completion
block.
Up until now, we have seen how to create a widget, animate it, and ensure it is updated as much as we can. But if we want to make our widget shine, we need to add some user-interactive capabilities.