This is a two-part article in which we will learn about WidgetKit for creating Widgets and the newly introduced ActivityFramework for creating Live Activities and Dynamic Island interaction in our iOS Apps.
In this part of the article, we will be learning about the WidgetKit framework, using which we will create widgets for our App named SA Blogs.
Before starting with the tutorial please download the starter project from here download.
This starter project contains a very basic static SwiftUI app that shows featured and latest posts for SwiftAnytime, you can look around the codebase, but the implementation has no impact on this tutorial.
We will be implementing a widget where users can see the latest, and featured articles from SwiftAnytime updated each hour directly to their Home Screen, the completed widget would look like this.
To implement Widgets for your app you will need to add App Extension called Widget Extension
Adding Widget Extension to your App
Creating a widget extension is the first step to adding support for widgets in your app you can use a single widget extension to add multiple widgets.
To add a widget extension to the starter app follow the below steps:
- Open SABlogs in Xcode and choose File > New > Target.
- Find the Application Extension section, select Widget Extension, and then click Next.
- Enter the name of your extension as SABlogWidgets.
- Check the Include Configuration Intent checkbox.
- Click Finish.
Creating a configurable widget
We can create a configurable widget using Siri intent with the widget kit, this gives users the flexibility to change provided parameters to customize the widgets
Creating the Widget
Inside the folder SABlogWidget
create a new file named SADynamicConfigurableWidget.swift
make sure that the target membership of this file is only set for the SABlogWidget
target.
Paste in below code in the file...
import WidgetKit
import SwiftUI
import Intents
struct SADynamicConfigurableWidget: Widget { // 1
let kind: String = "SADynamicConfigurableWidget" // 2
var body: some WidgetConfiguration { // 3
IntentConfiguration( // 4
kind: kind,
intent: ConfigurationIntent.self,
provider: <#IntentTimelineProvider#>
) { entry in
}
.configurationDisplayName("SA Blogs") // 5
.description("This is a configurable widget that shows featured and latest blogs from SwiftAnytime") // 6
}
}
Code Explaination:
- We created a struct conforming to the
Widget
protocol from the widget kit, this protocol is pretty similar to theView
property fromSwiftUI
- Here we declare a string id for our widget to get identified by the system
Widget
have one requirement which is a variable namedbody
of typesome WidgetConfiguration
- In the body property we are expected to pass a widget configuration, for making this widget configurable we return an
IntentConfiguration
this accepts the below values,
1. A string in kind
to recognise our widget,
2. An Siri Intent type, to populate the configuration view from when the user edits the widget, this ConfigurationIntent
get auto-generated by Xcode if you have checked the intent checkmark while creating the widget extension, we will be configuring this intent to accept our custom input later in the article
3. A object that conforms to IntentTimelineProvider
this object is responsible to provide the updated data TimelineEntry
when the widget updates, as well as handling the updation timeline,
4. It also accepts a closure this closure is called every time the widget updates with an entry that comes from the provider
.
5. We set a name to this widget configuration, that appears in the widget picker gallery
Creating timeline entry
TimelineProvider creates multiple timeline entries with dates, widgetkit uses this entry to populate the data for your widgets, when a widget kit needs to render a widget it calls the content block of IntentConfiguration
we created earlier and passes it to the TimelineEntry
object, this object should contain all the info required to update/render the widget.
You can add any properties that you want to use in the widgets view here, in our case we will be adding properties of posts to render the post details in widgets.
Create a new file named PostEntry
make sure to check the target membership only to the WidgetExtension target
Paste in the below code:
import Foundation
import WidgetKit
struct PostEntry: TimelineEntry { // 1
let date: Date // 2
let post: Post // 3
}
Code explanation:
- We are creating a TimeLine entry which requires as which requires to contain a date to identify when this entry was generated
- We add a date property as enforced by the
TimelineEntry
protocol from WidgetKit - We add a post property to populate the widget data from
Fixing target membership issue for Post Model
You will see an error in this line as we declared the Post model in our app target but trying to access it from the WidgetExtension target.
To resolve this issue you will have to add the Post.swift file to the Widget Extension target as well
Configuring the intent
We will use Siri intent to provide the edit functionality inside our widget, for that we will be accepting post categories from the intent, which will be defined as an enum in the intent definition.
Go Over to the intent SABlogsWidget.intentdefinition
generated by Xcode when you create the widget extension target

Here in the Customer Intents sidebar select the Configuration intent, it would be preselected as this is the only intent we have, inside the main view, go to the parameter section, and tap on the plus icon in the bottom right.

Name the parameter as a category
and keep the type String
, for now, it should look like this.

We will not rely on a string for our input rather we will create an enum for taking the user input for this we will be creating a custom type definition in intent.
In the left second sidebar, tap on the bottom left plus icon and select the enum

Name this enum as Category

Go to the main view and add two enum cases:
Once you are done adding the cases it is time for setting this custom enum type as our config type in intent, go to the Configuration
intent select the response and set the data type for our parameter named category
to Category enum.
Creating widget's timeline provider
The timeline suggests the WidgetKit when to update the widget, we will be using IntentTimelineProvider
which is similar to TimelineProvider
with the addition of having user-configured details in timeline entries which we will see later in this article.
Create a new file named SADynamicConfigurableTimelineProvider.swift
inside the SABlogWidget
folder, make sure to change the target membership to the widget extension
Now paste in the following code and build using CMD + B
struct SADynamicConfigurableTimelineProvider: IntentTimelineProvider {
}
Here we have created a struct conforming to IntentTimelineProvider, which have two associated type requirement one for the Entry
and one for Intent
which we will need to set the types for

Click on the fix button and it will populate the requirements, below

Set the type for Entry as our custom Entry Type PostEntry
and for Intent set it as ConfigurationIntent
This ConfigurationIntent
type is generated by Xcode from our intent definition file, the file should contain the below code.
struct SADynamicConfigurableTimelineProvider: IntentTimelineProvider {
typealias Entry = PostEntry
typealias Intent = ConfigurationIntent
}
Try to build the project again, and click on the fix:

Once you do Xcode will prepopulate the required functions stubs by this time you can optionally remove the type aliases:
The struct will look like this:
struct SADynamicConfigurableTimelineProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> PostEntry {
}
func getSnapshot(
for configuration: ConfigurationIntent,
in context: Context,
completion: @escaping (PostEntry) -> Void
) {
}
func getTimeline(
for configuration: ConfigurationIntent,
in context: Context,
completion: @escaping (Timeline<PostEntry>
) -> Void) {
}
}
These respective functions are called by the WidgetKit framework at a different point in time as required to populate the widget.
Setting the placeholder
placeholder(in:)
is called when our widget is either shown for the first time when the Time Line is not yet populated or the widget is shown in the widget library for the first time, here widget kit passes us a context object by using which you can check where the widget is being shown, as well as the family type of widget being show e.g system small, system medium etc.
In this function you need to pass an Entry that will be used to render the placeholder widget, we will pass an empty entry for this:
func placeholder(in context: Context) -> PostEntry {
PostEntry( // 1
date: Date(), // 2
post: Post( // 3
image: FileManager.default.temporaryDirectory,
title: "Title..",
description: "Description.."
)
)
}
Code Explanation:
- We initialised an Entry using PostEntry's initialiser,
- Pass the current date for this entry,
- Pass dummy/placeholder data in the initialiser.
Implementing the snapshot
getSnapshot(for:in:completion:)
this function is responsible to provide a timeline entry representing the current time and state of a widget, this gets called from WidgetKit during transient situations (Intermediate situation which lies where there is no timeline entry exists, e.g in WidgetGallery), this passes the context as well, one key point to take into consideration here is if the context.isPreview
is true you should bypass the actual data loading if it takes more than few seconds and calls the completion with dummy data otherwise the widget in widget gallery can glitch.
We will be using this method to return a Post Entry according to the user's intent configuration Using the PostRepository
class, add the following static property in SADynamicConfigurableTimelineProvider
static var repository = PostRepository()
If you try to build your widget extension target right now Xcode will show an error saying Cannot find 'PostRepository' in scope
this is because the file PostRepository.swift
have target membership to the host app only and not to the WidgetExtension target, to fix this Go to the file PostRepository.swift and in the target membership section adds the checkmark for widget extension as well.
You will require the add PostCategory.swift
to the widget extension target as well.
Once this is done come back to SADynamicConfigurableTimelineProvider.swift
and add the following implementation in getSnapshot(for:in:completion:)
function..
func getSnapshot(
for configuration: ConfigurationIntent,
in context: Context,
completion: @escaping (PostEntry) -> Void
) {
let posts: Posts
if configuration.category == .featured { // 1
posts = Self.repository.featuredPosts
} else {
posts = Self.repository.latestPosts
}
let date = Date()
let postIndex = Int(date.timeIntervalSince1970) % (posts.count-1) // 2
let postEntry = PostEntry(date: date, post: posts[postIndex]) // 3
completion(postEntry) // 4
}
Code Explaination:
- Here we use the passed intent configuration to check the post category selected by the user, and set the value of the posts according to the user config
- We calculate the post index using an arbitrary formula on the date
- We use the index calculated to get the post object from the array and create a PostEntry using it
- After this we call the completion passing the post entry
Providing widget update timeline
getTimeline(for:in:completion:)
Is responsible for providing an array of timeline entries, and the next update time, In this function you are expected to pass multiple entries containing the data for rendering the widget in a particular time,
We will be configuring our widget to have PostEntry every other hour for five hours and once that time frame is completed we will remake the PostEntry sequence for the next five hours.
func getTimeline(
for configuration: ConfigurationIntent,
in context: Context,
completion: @escaping (Timeline<PostEntry>
) -> Void) {
let posts: Posts
if configuration.category == .featured { // 1
posts = Self.repository.featuredPosts
} else {
posts = Self.repository.latestPosts
}
let currentDate = Date()
var entries: [PostEntry] = []
for hourOffset in 0 ..< 5 { // 2
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let postIndex = Int(entryDate.timeIntervalSince1970) % (posts.count-1)
let entry = PostEntry(date: entryDate, post: posts[postIndex])
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd) // 3
completion(timeline) // 4
}
Code Explanation:
- We switch the post array according to the widget configuration passed
- We create an array for PostEntry each having date property one at the one-hour difference to consecutive PostEntry
- We create a timeline with the array of entries and pass
TimelineReloadPolicy.atEnd
for TimelineReloadPolicy this policy tells the widget kit what to do after our current return time get finished, you can either pass.atEnd
to let the widget kit know you want to get this functiongetTimeline(for:in:completion:)
after the timeline ended, you can pass a particular date as well after which this function should be called regardless the current timeline is finished or not by sending,TimelineReloadPolicy.after(_ date: Date)
with the date, you also passTimelineReloadPolicy.never
to let widget kit know that you don't want to create any more timelines. - Then we call the completion with the timeline object
At this step, your SADynamicConfigurableTimelineProvider.swift
file should look like this.
import Foundation
import WidgetKit
struct SADynamicConfigurableTimelineProvider: IntentTimelineProvider {
static var repository = PostRepository()
func placeholder(in context: Context) -> PostEntry {
PostEntry(
date: Date(),
post: Post(
image: FileManager.default.temporaryDirectory,
title: "Title..",
description: "Description.."
)
)
}
func getSnapshot(
for configuration: ConfigurationIntent,
in context: Context,
completion: @escaping (PostEntry) -> Void
) {
let posts: Posts
if configuration.category == .featured {
posts = Self.repository.featuredPosts
} else {
posts = Self.repository.latestPosts
}
let date = Date()
let postIndex = Int(date.timeIntervalSince1970) % (posts.count-1)
let postEntry = PostEntry(date: date, post: posts[postIndex])
completion(postEntry)
}
func getTimeline(
for configuration: ConfigurationIntent,
in context: Context,
completion: @escaping (Timeline<PostEntry>
) -> Void) {
let posts: Posts
if configuration.category == .featured {
posts = Self.repository.featuredPosts
} else {
posts = Self.repository.latestPosts
}
let currentDate = Date()
var entries: [PostEntry] = []
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let postIndex = Int(entryDate.timeIntervalSince1970) % (posts.count-1)
let entry = PostEntry(date: entryDate, post: posts[postIndex])
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
Creating the widget entry view
We will be creating a really simple swiftui view for our widget, it would contain a post image and the title and subtitle.
Create a file name SADynamicConfigurableWidgetEntryView.swift
with target membership to the widget extension.
And paste in the below code
import SwiftUI
import WidgetKit
struct SADynamicConfigurableWidgetEntryView : View {
var entry: SADynamicConfigurableTimelineProvider.Entry
var body: some View {
VStack(alignment: .leading) {
Text(entry.post.title).font(.headline)
Text(entry.date, style: .time).font(.subheadline)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
.background(.thinMaterial)
.clipShape(ContainerRelativeShape())
.padding(8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.background(
Image("background")
.resizable()
.scaledToFill()
)
}
}
struct SADynamicConfigurableWidgetEntryView_Previews: PreviewProvider {
static var previews: some View {
ForEach(
[
WidgetFamily.systemSmall,
WidgetFamily.systemMedium,
WidgetFamily.systemLarge
], id: \.self) { family in
SADynamicConfigurableWidgetEntryView(
entry: PostEntry(
date: Date(),
post: Post(
image: URL(string: "https://www.swiftanytime.com/content/images/size/w1200/2022/07/asyncimage-swiftui.jpg")!,
title: "AsyncImage in SwiftUI", description: "Almost all modern apps and websites are driven by images and videos. The images you see on shopping apps"
)
)
)
.previewContext(
WidgetPreviewContext(family: family)
)
}
}
}
Preview:

Integrating the TimelineProvide and EntryView in the Widget
We have prepared all the necessary ingredients for your widget recipe now it's time to cook them, but first we need to do a bit of cleaning up and delete the file generated by Xcode while creating the Widget Extension target, named SABlogsWidget.swift
, once this is deleted go to -> SADynamicConfigurableWidget.swift
file and replace the code with below
import WidgetKit
import SwiftUI
import Intents
@main // 1
struct SADynamicConfigurableWidget: Widget {
let kind: String = "SADynamicConfigurableWidget"
var body: some WidgetConfiguration {
IntentConfiguration(
kind: kind,
intent: ConfigurationIntent.self,
provider: SADynamicConfigurableTimelineProvider() // 2
) { entry in
SADynamicConfigurableWidgetEntryView(entry: entry) // 3
}
.supportedFamilies([.systemSmall,.systemMedium,.systemLarge]) // 4
.configurationDisplayName("SA Blogs")
.description("This is a configurable widget that shows featured and latest blogs from SwiftAnytime")
}
}
Code Change Explanation:
- We added the
@main
attribute to mark this file as the start of the execution of this WidgetExtension - We created an instance of SADynamicConfigurableTimelineProvider and passed it to the configuration initialiser
- Then we returned the view we just created
- And added the supported widget families
Result
|
|
---|
You can download the completed project here download.