WidgetKit was first introduced in iOS 14 as a simple framework whereby developers can leverage to create a Home Screen Widget. Since then, it has slowly evolved into a framework that powers some of the most anticipated features in iOS 16, namely Lock Screen Widget, Live Activities, and Dynamic Island.
It is safe to say that WidgetKit has now become one of the must-learn frameworks for all the iOS developers out there. Therefore, I have decided to create a series of articles that center around WidgetKit and other related topics. This article will be the first in the series, so let’s take things one step at a time by looking into the basics you need to know to get started creating your first Home Screen Widget.
There is a lot to unpack here, so let’s get started.
The View Size Widget
In this article, we will build a simple Home Screen Widget that displays the dimension of the widget. On top of that, the widget will also display the timeline provider information which we will take a closer look at later on.
Adding a Widget Extension
All iOS widgets must tie to an iOS app. Once you have your iOS app created in Xcode, go ahead and add a new widget extension target and name it “ViewSizeWidget”. For simplicity’s sake, leave “Include Configuration Intent” unchecked as it is beyond the scope of this article. I will be covering that in detail in a future article, so please stay tuned.
Upon adding the new target, Xcode will prompt you to activate the scheme for the widget extension. Go ahead and activate it.
At this stage, you should notice that a folder with a Swift file (ViewSizeWidget.swift
) has been added to your Xcode workspace.
Go ahead and delete all the auto-generated code in ViewSizeWidget.swift
. In the following sections of this article, I will guide you through the entire implementation process of ViewSizeWidget.swift
.
Creating a Widget
A widget consist of 4 main components:
- The Timeline Entry
- The Widget View (SwiftUI View)
- The Timeline Provider
- The Widget Configuration
Let’s implement each of these components one-by-one.
The Timeline Entry
You can imagine the timeline entry as the model object of the widget view. It must at least consist of a date
parameter. Any other parameters can also be added to the timeline entry based on our own needs.
The system will then use the date specified in the timeline entry to determine when to show/refresh the data in the widget.
For our view size widget, let’s name our timeline entry ViewSizeEntry
.
import WidgetKit
import SwiftUI
struct ViewSizeEntry: TimelineEntry {
let date: Date
let providerInfo: String
}
Notice that we will use the providerInfo
to hold the information related to the timeline provider and show it on the widget — more on that later.
The Widget View
After creating our timeline entry, we can proceed to implement the Widget’s UI. It is basically just a SwiftUI View.
struct ViewSizeWidgetView : View {
let entry: ViewSizeEntry
var body: some View {
GeometryReader { geometry in
VStack {
// Show view size
Text("\(Int(geometry.size.width)) x \(Int(geometry.size.height))")
.font(.system(.title2, weight: .bold))
// Show provider info
Text(entry.providerInfo)
.font(.footnote)
}
.containerBackground(for: .widget) {
Color.green
}
}
}
}
There is nothing fancy about the code above. But I do like to draw your attention to the entry
parameter. Notice how we are using it to show the data (providerInfo
) in our timeline entry onto the widget UI.
Also, notice how we are using the containerBackground(for:alignment:content:)
view modifier to define the background of the widget view. You can find out more here.
The Timeline Provider
As the name implies, the timeline provider provides information to the system about what is to be shown on the widget at a specific timestamp. As you might have expected, the timeline provider must conform to the TimelineProvider
protocol which has the following 3 method requirements.
struct ViewSizeTimelineProvider: TimelineProvider {
typealias Entry = ViewSizeEntry
func placeholder(in context: Context) -> Entry {
// Implementation here...
}
func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
// Implementation here...
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// Implementation here...
}
}
Also, be sure to alias Entry
to the ViewSizeEntry
we defined not long ago.
Now let’s go ahead and implement these 3 methods one-by-one.
First off is the placeholder(in:)
method. It is essentially providing dummy data to the system to render a placeholder UI while waiting for the widget to get ready. Notice that SwiftUI will apply a redaction effect to the dummy data we provide, thus the actual value of the dummy data is not important. Here’s the implementation:
func placeholder(in context: Context) -> Entry {
// This data will be masked
return ViewSizeEntry(date: Date(), providerInfo: "placeholder")
}
which will yield the following placeholder UI:
Next up is the getSnapshot(in:completion:)
method. This function mainly provides the data required by the system to render the widget in the widget gallery. Following is the implementation:
func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
let entry = ViewSizeEntry(date: Date(), providerInfo: "snapshot")
completion(entry)
}
From the screenshot above, you can see that the widget is actually showing the providerInfo
we provided — “snapshot”.
Lastly is the getTimeline(in:completion:)
method. This is the most important method in the timeline provider as it provides an array of timeline entries for the current time and, optionally, any future times to update a widget.
Since our widget is just showing static data all the time, we can simply return 1 timeline entry like so.
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entry = ViewSizeEntry(date: Date(), providerInfo: "timeline")
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
With all that in place, we can finally proceed to configure the widget.
The Widget Configuration
The widget configuration is where we put everything we just implemented together. Let’s take a look at the implementation and I will go through the details later on.
@main
struct ViewSizeWidget: Widget {
let kind: String = "ViewSizeWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ViewSizeTimelineProvider()) { entry in
ViewSizeWidgetView(entry: entry)
}
.configurationDisplayName("View Size Widget")
.description("This is a demo widget.")
.supportedFamilies([
.systemSmall,
.systemMedium,
.systemLarge,
])
}
}
The first thing to notice here is the @main
attribute. It indicates that the ViewSizeWidget
is the entry point for our widget extension, implying that the extension contains a single widget. For cases where we have multiple widgets, which is beyond the scope of this article, we will have to mark a widget bundle as an entry point.
The main purpose of a widget configuration is to link up the timeline provider and the widget view. The kind of configuration we are using here is StaticConfiguration
. Using StaticConfiguration
means that our widget does not have any user-configurable properties. We will need to use IntentConfiguration
instead if we would to add any user-configurable properties to our widget.
There are various types of modifiers we can use to further configure the widget configuration. For example, we can use the configurationDisplayName
and description
modifier to configure the widget gallery title and subtitle.
Lastly, the supportedFamilies
modifier allows us to specify the widget size that we want to support. For our view size widget, we will be supporting small, medium, and large sizes. Note that there are other widget families such as accessoryCircular
, accessoryRectangular
, etc. that are mainly used for creating Lock Screen Widgets. I will be covering them in depth in an article about Lock Screen Widgets, so make sure to keep an eye out!
With all these in place, we have completed the view size widget implementation. Now go ahead and run the app using the widget extension scheme to see everything in action.
The full sample code is available here.
Wrapping Up
What you’ve learned in this article are the very basics of WidgetKit, there is still a lot more to cover and explore. Be sure to expect more articles with topics surrounding WidgetKit coming soon to this site. Feel free to follow me on Twitter and subscribe to my newsletter so that you won’t miss out on any of my upcoming 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.