Updated for Xcode 12 beta 6
Home screen widgets were one of the flagship features introduced for iOS and MacOS at WWDC20, they're a powerful extension allowing users a window into your app when it's not open in the foreground.
In this article we're going to build a simple home screen widget that shows a movie to the user, it will periodically update with a new movie from a local source and refresh the UI.
Widgets come in three sizes, .systemSmall
, .systemMedium
and .systemLarge
, and you can provide a different layout for each if you choose. In this article we're going to build a one size fits all layout, but i'll also show you how you can provide different SwiftUI views based on the size.
The full code for this article can be found at https://github.com/Swift-Compiled/homescreen-widgets
By the end of this article you'll have something that looks like this...

Introduction to WidgetKit
Although widgets have been available in previous versions of iOS, home screen widgets are new in iOS14 and are made available to developers with the introduction of WidgetKit, Apple's new framework providing lightweight, performant SwiftUI views for our home screen. According to Apple, the previous way of working with widgets (in the today view) had performance constraints that make them unsuitable for home screen use, home screen widgets have been made possible since the intrduction of SwiftUI at WWDC19.
The entry point to a Widget is the Widget
protocol, at first glance it has a similar structure to a SwiftUI view, with a body
being its only requirement that returns a type WidgetConfiguration
, which in turn provides a SwiftUI view inside a closure. A WidgetConfiguration
can either be of type StaticConfiguration
or IntentConfiguration
, to understand how they work you'll first need to know about the timeline...
The timeline is a new concept in WidgetKit
, you can think of a timeline as a type who's job it is to provide the state of your widget over time.

Starting from the left in this image, the WidgetKit extension is providing an array of updates on the timeline that reflect your widget at any given time, these updates are controlled by the developer. For example, a calendar widget would provide an array of all events for the day as multiple timeline entries, and as one event passes the UI would update with the next event in the list. When a user taps your widget it will open your app, but that's where the user interaction ends, you can't add buttons, sliders or other interactive UI.
At the time of writing you'll need to be running the Xcode 12 beta to work with WidgetKit
Open Xcode 12 and start a new project, select the new App
menu item in the Application section, select the Multiplatform
app template in the top left, call the project MoviePlanner.

Once in Xcode, navigate to the project explorer by clicking on the MoviePlanner
project file, under Targets
click the plus button at the bottom and search for Widget in the search bar of the new target template screen, select Widget Extension
and call it MoviePlannerWidget
. Once finished Xcode will ask if you want to activate the new scheme, let's do that as we don't need to touch the core app code at all for this article.
We should now have a new directory in our project structure on the left called 'Movie Planner Widget', Xcode has conveniently added the boilerplate code for us!
Our app is going to store a short list of movies and cycle through them over time using a timer, as the movie updates so will our widget.
The first thing we need to do is define our model, for this example it's only going to need a title and an image name. Add a new file called Movie
and add the following.
struct Movie {
let name: String
let movieImageName: String
static var empty: Movie {
Movie(name: "", movieImageName: "")
}
}
Make sure Movie has a target membership of MoviePlannerWidgetExtension
by ticking the box in the File Inspector menu on the right hand side.

Next, download the three images below and add import (drag and drop) directly into Assets.xcassets
inside the MoviePlannerWidget
directory, don't import them into the asset catalogue inside Shared
as this is currently inaccessible to our widget.



From left to right these images should be named endgame
, gog3
and infinity
, if these names don't match rename them before importing them into your project.
Next create a new file called MovieUpdater
, this will be responsible for s
toring an array of Movie
objects. We're going to add three movies to start with, but you can add as many as you'd like.
class MovieUpdater {
var movie: Movie = Movie.empty
var movies: [Movie] = [
Movie(name: "Avengers: Infinity War",
movieImageName: "infinity"),
Movie(name: "Avengers: End Game",
movieImageName: "endgame"),
Movie(name: "Guardians of the Galaxy Volume 3",
movieImageName: "gog3")
]
}
Ok, that's all the setup we need, now we can get going with the widget.
Open MoviePlayerWidget.swift
and scroll down to the MoviePlannerWidget
declaration, this is the entry point to our widget. It conforms to the Widget
protocol who's only requirement is that it have a body
that returns a WidgetConfiguration
. There are two types of configurations available, IntentConfiguration
and StaticConfiguration
. The former allows the user to perform some configuration and takes a little more to setup, whereas the latter does not, we'll be focusing in StaticConfiguration
in this article, i'll cover IntentConfiguration
in part two (coming soon!).
Scroll up slightly and look at the SimpleEntry
struct, let's change this to be more specific to our Movie Widget. Replace it with the following.
struct MovieEntry: TimelineEntry {
public let date: Date
public let movie: Movie
}
MovieEntry
is of type TimelineEntry
, this is what the Timeline uses to update the widget, it requires a date to conform to TimelineEntry
and we're going to give it a Movie
so our UI has access to the current movie.
By default Apple provides us with an InentConfiguration
, let's change this to StaticConfiguration
, replace the contents of MoviePlannerWidget
with the following.
@main
struct MoviePlannerWidget: Widget {
private let kind: String = "MoviePlannerwidget"
public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind,
provider: Provider()) { entry in
MoviePlannerWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
the only change we've made here is switching out IntentConfiguration
for StaticConfiguration
, you'll notice we're still getting build errors though, so what gives??
Scroll up to Provider
and you'll see a myriad or errors, we renamed SimpleEntry
to MovieEntry
and we changed IntentConfiguration
to StaticConfiguration
which means Provider
now needs to conform to a different protocol. Replace IntentTimelineProvider
in the declaration of Provider
with plain old TimelineProvider
, delete the contents of Provider
and replace it with the following.
struct Provider: TimelineProvider {
typealias Entry = MovieEntry
var movieUpdater: MovieUpdater = MovieUpdater()
func placeholder(in context: Context) -> MovieEntry {
return MovieEntry(date: Date(), movie: Movie.empty)
}
func getTimeline(in context: Context,
completion: @escaping (Timeline<Entry>) -> Void) {
var entries = [MovieEntry]()
var nextUpdateDate = Date()
for (i, movie) in movieUpdater.movies.enumerated() {
nextUpdateDate = Calendar.current.
date(byAdding: .minute,
value: 1 * i, to: Date())!
let entry = MovieEntry(date: nextUpdateDate, movie: movie)
entries.append(entry)
}
let timeline = Timeline(
entries: entries,
policy: .atEnd
)
completion(timeline)
}
func getSnapshot(in context: Context,
completion: @escaping (Entry) -> Void) {
let date = Date()
let entry = MovieEntry(date: date, movie: Movie.empty)
completion(entry)
}
}
This may look a little daunting, but all the logic we need to render our widget is in here, the Provider
struct bridges the gap between our code and our UI by providing a timeline.
TimelineProvider
requires us to define a type for Entry
, in our case we need it to be of type MovieEntry
so we set that up first with typealias Entry = MovieEntry
.
Next we implement placeholder(in context:)
and return a MovieEntry
object with an empty movie. This tells WidgetKit what to render while the widget is loading.
The job of getTimeline(context:completion:)
is to provide a Timeline
object populated with entries, in this case we're providing a single entry for each movie using the MovieEntry
type we setup earlier. We're constructing a date for each entry that's 1 minute after the last. We then tell WidgetKit
to ask for a new timeline once the last date has passed by using the .atEnd
update policy. There are currently three update policies available in WidgetKit:
atEnd
asks for a new timeline after the last timeline entry date passes..after(_)
allows us to pass in a date at which the timeline should be refreshed.never
relies on the app to refresh the timeline manually by calling one of the built in reload functions. For exampleWidgetCenter.shared.reloadAllTimelines()
.
I've had mixed results working with reload policies, even now there seems to be a delay when the last date passes in the timeline and the reload, this will hopefully be fixed in future betas
The job of getSnapshot(context:completion:)
is to return our view, we're using local data here but if we were querying a server we could check to see if the widget is in preview mode by interrogating context.isPreview
and provide sample data, this way our widget would load instantly. We don't need to do that here as our data is all local, but it's useful to know. Context
can also provide you with the widget family, an emum
denoting the current size, .systemSmall
, .systemMedium
or .systemLarge
, we can use this to render a different UI for each if we choose.
The final step is the UI. Replace the contents of MoviePlannerWidgetEntryView
with the following.
struct MoviePlannerWidgetEntryView : View {
var entry: Provider.Entry
var gradient: Gradient {
Gradient(stops:
[Gradient.Stop(color: .clear, location: 0),
Gradient.Stop(color: Color.black.opacity(0.95),
location: 0.6)])
}
var body: some View {
ZStack {
Image(entry.movie.movieImageName)
.resizable()
.aspectRatio(contentMode: .fill)
.mask(
RoundedRectangle(cornerRadius: 10)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: 0, maxHeight: .infinity)
)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minHeight: 0, maxHeight: .infinity)
VStack {
Spacer()
LinearGradient(gradient: gradient,
startPoint: .top,
endPoint: .bottom)
.frame(height: 100)
}
VStack(alignment: .leading) {
Spacer()
Text(entry.movie.name)
.padding(.all, 10)
.font(.headline)
.foregroundColor(.white)
}
}
}
}
Here we're using a ZStack
with an Image
to display an image to cover the full widget background, then two VStack
s, one to display a gradient so you can see the text on the image and the second to display the movie title. The Spacer
views we use here make sure the gradient and text are pushed right to the bottom of the widget.
Now in Xcode make sure you select the MoviePlannerWidgetExtension
target.

Run the app in the simulator, the widget will appear on your home screen and it will update with a new movie every 10 seconds.
Nice work! A lot of what we've covered in this article is the setup of our Movie
and MovieUpdater
, in a real app you'd likely have this up and running before you built your widget. It takes a surprisingly little amount of code to create home screen widgets with WidgetKit, and the results can be powerful extensions for your users.
Where to go from here
This was a quick introduction into the power of WidgetKit, in part two i'll use the same example to show how we can use IntentConfiguration
so stat tuned for that!
As mentioned previously you can use Context
to check the size of the widget, try adding different UI for different sizes, for example you could display all movies in a list for .systemLarge
, or add a release date to the model and display it when in .systemMedium
, it's fun to experiment and i'd love to see your results!
The full code for this article can be found at https://github.com/Swift-Compiled/homescreen-widgets
Thanks for reading ✌️