Play with iOS Widgets

Bo Liu
Axel Springer Tech
Published in
4 min readMar 22, 2024

--

Background

In WWDC2020, Apple released WidgetKit to replace the old Today Extension. It can bring many benefits, such as that you can put it anywhere instead of Today screen, different types of size. Apple also announced this is the biggest update for the home screen in iOS history.

Nevertheless, I was not interested in it at the beginning since there were some limits to it compared with Android Widgets. First, users could not interact with Widgets, it just displayed some information (even Today was interactable). Second, there's a limit to refreshing the UI, the minimum timeline interval is only 5 minutes, so it's not easy to implement some real-time features. Following the iOS upgrade, it starts to support live activities and interactivity. Everything becomes fun!

Why is Apple Widget important to an App?

My answer is visibility.

Research shows that there are 80+ apps installed on the average smartphone. Nowadays, it's not easy to let users find your app on home screen and then open it, especially, since users can put the apps that they don't often use in App Libray now.

However, Widgets are eye-catching stuff. They are bigger than normal icons and display useful information. Users can see it anywhere, whether home screen or lock screen. The app can always remind users "Hey, I am here, feel free to open me!". Every user only has 24 hours per day, so every app needs to be exposed more to compete for users' only usage time.

Widgets of WELT News

How to implement a static configuration Widget

There are two types of widgets from a configuration perspective, with static Configuration and App Intent Configuration. For App Intent Configuration, users can press the edit button to set up some extra parameters for widgets while static one cannot.

Regarding static configuration, you can view it as a simple view, and after some interval, it will refresh its content. Here need to introduce two concepts: TimelineEntry and TimelineProvider.

TimelineEntry

TimelineEntry contains a specific date to render widgets, and some data is provided to the widget view. You also can define a model inside for widget.

TimelineProvider

TimelineProvider is a protocol where we can make our render strategy by a timeline. The logic should be implemented in func getTimeline(in _: Context, completion: @escaping (Timeline<Entry>) -> Void). Like the graph below, we can schedule time every 1 hour to render widgets, and afterwards, never refresh the timeline.

When reloading timeline, we can request data such as from endpoint. It's also the unique place where you can do async tasks (even including async load images). Then, we need to convert data to a series of TimelineEntry to provide to Timeline. We also must tell Timeline when we reload timeline next time. There're three policies for it:

  1. atEnd: after the last date in a timeline, WidgetKit will request a new timeline
  2. never: schedule no next timeline except force to refresh widgets
  3. after: reload timeline in a specific future time

Besides, we also need to care about other two functions in TimelineProvider:

  1. placeholder: When Widgets crash or no data, it will display a skeleton placeholder in terms of the data we provide.
  2. getSnapshot: In the add widget screen, users can preview the widgets by the data we provide.

EntryView

When we finish preparing the Timeline, the next step is to render the widget view. The entry view should be written with SwiftUI. It includes a field to receive TimelineEntry. Meanwhile, we need to describe the types of widgets here depending on our requirements. For example, if we only want small, medium, and large sizes widgets on home screen, we can declare like this:

struct WidgetExtensionEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) private var family
var body: some View {
Group {
switch family {
case .systemSmall:
SmallWidgetView(widgetArticleList: entry.widgetArticleList)

case .systemMedium:
MediumWidgetView(widgetArticleList: entry.widgetArticleList)

case .systemLarge:
LargeWidgetView(widgetArticleList: entry.widgetArticleList)

default:
EmptyView()
}
}
.animation(.spring, value: entry.widgetArticleList)
}
}

Now, it's time to combine the timeline and widget view. The Widget body requires a WidgetConfiguration. For static configuration, we should provide a kind string as identification and tell the provider instance to StaticConfiguration. Learn about via below example:

public struct RegularWidgets: Widget {
let kind: String = "RegularWidgets"

public var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: WidgetExtensionProvider()
) { entry in
if #available(iOS 17.0, *) {
WidgetExtensionEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
WidgetExtensionEntryView(entry: entry)
.background()
}
}
.configurationDisplayName("Widget_Name")
.description("Widget_Description")
.contentMarginsDisabled()
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}

PS: If you don't want to have the native margin of widget UI, please add .contentMarginsDisabled() to avoid it.

Currently, we apply the timeline for three types of widgets. You also can define more Widgets to map different timeline strategies. Just register all the Widgets you defined in WidgetBundle, and you will see them.

import WidgetKit

@main
struct WidgetExtensionBundle: WidgetBundle {
var body: some Widget {
RegularWidgets()
AccessoryWidgets()
IPadWidgets()
}
}

We finished a static configuration widget so far, but it just started the whole journey of WidgetKit development. See you in the next article!

--

--