You are currently viewing How to Update or Refresh a Widget?

How to Update or Refresh a Widget?

Due to the always-on nature of a widget, Apple has introduced a very specific way for developers to refresh a widget in order to ensure its power efficiency and prevent developers from exploiting the processing power of a device. 

In this article, I would like to show you in detail what it takes to efficiently refresh your widgets, as well as some best practices to follow when refreshing your widgets.

So without further ado, let’s begin!


The Countdown Widget

As usual, let’s use a sample widget I recently created to showcase the ways to update or refresh a widget.

The countdown widget

As you can see, the sample widget will start with a 60s countdown. Once the countdown ends, the widget will immediately start the next countdown cycle.

By using this countdown widget, we can easily monitor how a widget is being updated and what will happen after a refresh session ends.


Showing Dynamic Dates in Widgets

The first method we can use to refresh a widget is pretty easy, but it only works for displaying dynamic dates. The idea is to leverage the Text view in SwiftUI and let the system handle the refresh for us.

For example, if we want to display a 60s countdown:

let components = DateComponents(second: totalCountdown + 1)
let futureDate = Calendar.current.date(byAdding: components, to: Date())!

// Show dynamic date in Text view
Text(futureDate, style: .timer)

With that in mind, displaying a countdown timer in a widget is pretty straightforward. 

struct CountdownWidgetView: View {
    
    var body: some View {
        Text(getFutureDate(), style: .timer)
            .monospacedDigit()
            .padding(4)
            .font(.headline)
            .multilineTextAlignment(.center)
            .background(.red)
    }
    
    private func getFutureDate() -> Date {
        let components = DateComponents(second: 60)
        let futureDate = Calendar.current.date(byAdding: components, to: Date())!
        return futureDate
    }
}

Here’s how it looks like:

A countdown timer

Pro Tip:

Check out this article by Apple to learn more about showing up-to-date, time-based information in your widget even when it isn’t running.


Updating Widgets with Timeline Provider

Now that we have created the countdown timer for our countdown widget. Let’s switch our focus to the other UI elements in the widget. For UI elements that are not showing dynamic dates, we will need to use the timeline provider.

First thing first, let’s define the timeline entry which will be consumed by the timeline provider.

struct CountdownEntry: TimelineEntry {
    let date: Date
    let status: String
    let gaugeValue: CGFloat
}

The CountdownEntry‘s parameters are pretty self-explanatory:

  • date: The date and time to refresh the widget
  • status: The widget’s current status
  • gaugeValue: The number displayed at the center of the gauge
The iOS countdown widget's parameters
The countdown widget’s parameters

The Timeline Provider Implementation

It is mandatory for us to implement 3 methods when creating a timeline provider. For this article, I will be focusing on implementing the getTimeline(in:completion:) method. For the other 2 methods, you can find out more here.

The main purpose of getTimeline(in:completion:) is to let the system know when and what to update. This can be done by feeding the system with a timeline object consisting of an array of timeline entries.

For our countdown widget, 1 timeline object will represent a countdown from 60 to 0. In other words, 1 timeline object will consist of 61 timeline entries with each entry separated 1s apart. With that in mind, we can implement the getTimeline(in:completion:) method like so:

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    let totalCountdown = 60
    
    // Entries required for 1 countdown session (60 seconds)
    var entries = [CountdownEntry]()
    
    // Generate 61 entries to show countdown from 60 - 0
    for i in 0...totalCountdown {
        
        // Determine widget refresh date & create an entry
        let components = DateComponents(second: i)
        let refreshDate = Calendar.current.date(byAdding: components, to: Date())!
        
        // Calculate gauge value
        let gaugeValue = CGFloat(totalCountdown - i)
        
        // Determine status
        // Status will become "Waiting..." when `gaugeValue` reached zero
        let status = gaugeValue == 0 ? "Waiting refresh..." : "Counting down..."
        
        let entry = CountdownEntry(
            date: refreshDate,
            status: status,
            gaugeValue: gaugeValue
        )
        
        entries.append(entry)
    }
    
    // Create a timeline using `entries`
    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
}

From the code above, notice how the timeline entries are being generated using a for-loop. Once everything is generated, we can then create a timeline object and trigger the completion handler.

Every time when the completion handler is called will mark the start of a refresh session. Within a refresh session, the widget will refresh based on the date in each timeline entry. Once the last timeline entry is processed, the refresh session ends.

The following image illustrate a refresh session of the countdown widget:

An iOS widget refresh session
The refresh session cycle

At this stage, you might wonder how we can let the system know when to start the next refresh session. This is where the refresh policy comes into play.

What Is Refresh Policy?

As of the time of writing this article, Apple offers 3 types of refresh policies:

  1. atEnd: WidgetKit can start the next refresh session right after the previous one is completed. There is no guarantee the next session will start immediately as the system will decide the best time to kick off the next refresh session.
  2. after(Date): WidgetKit can only start the next refresh session after a specific date and time. Just like atEnd, the system will decide when is the best time to start the next refresh session.
  3. never: WidgetKit will not start another refresh session until we explicitly call reloadTimelines(ofKind:) in the hosting app. The refresh session will start immediately after reloadTimelines(ofKind:) is called.

For our countdown widget, since we are using atEnd, you can see that the next countdown cycle starts right after the previous one ends.

To force it to start 5s after the previous cycle ends, we can do it like so:

// Next refresh session start 5s after the last entry
let lastUpdate = entries.last!.date
let nextUpdate = Calendar.current.date(byAdding: DateComponents(second: 5),
                                       to: lastUpdate)!

// Create a timeline using `entries`
let timeline = Timeline(entries: entries, policy: .after(nextUpdate))

Here’s the countdown widget’s full implementation (You can find the source code in the “CountdownWidget” folder).


The Best Practices When Refreshing Widgets

When running on a debugger, WidgetKit does not impose any limitation on the widget’s refresh rate. Meaning we can refresh the widget as frequently as possible. That’s why we are able to refresh our countdown widget every second and start the next countdown cycle right after the previous cycle ends. This is especially useful during development and implementation.

On the other hand, as stated in the Apple documentation, a frequently viewed widget can have a maximum of 40 to 70 refreshes per day when running in a production environment, which roughly translates to a widget refreshing every 15 to 60 minutes.

Notice that the above mentioned intervals will most likely vary due to various kinds of factors. Therefore, it is best for us to follow the best practices given by Apple when populating a timeline:

  1. Populate a timeline with as many future dates (timeline entries) as possible.
  2. Keep the interval of timeline entries in the timeline as large as possible.
  3. Ensure that each timeline entries in a timeline are at least about 5 minutes apart.

There you have it! That’s how you can effectively refresh a widget while preserving performance and battery life.

If you enjoy reading this article, feel free to check out my other articles related to WidgetKit. You can also follow me on Twitter, and subscribe to my newsletter so that you won’t miss out on any of my upcoming iOS development-related articles.

Thanks for reading. 👨🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.