This article is about one of the new features of iOS 14: Widgets. I’ll show you how to write a simple widget in SwiftUI, and how to update the widget data from your iOS app.
As usual, all the code is available on GitHub.
Intro
What is a Widget? Is an extension to your app, showing content in the home screen and giving the user the ability to have a small interaction with your app. I said small, as is not yet possible to perform operations in the Widget itself, you can go back to your application on a specific view by passing a URL, but that’s it.
Widgets require iOS 14, and you must use SwiftUI, there isn’t a UIKit equivalent API.
To build our first widget we’ll do something really simple. A SwiftUI app will feature a TextField and a Button, every time the button is pressed the widget updates with the text entered in the field.
I’ll show you how to add a simple widget first and at the end how to send updates to the widget via app group.
Add a widget
In order to build a widget, you have to add a new extension to your project.
Go to File -> New -> Target… and select Widget extension.
Click next and select a name for the widget, in my example I chose MyWidget. There is a checkbox “Include configuration Intent”, leave it off for now. There are two type of configuration for a Widget: Intent and Static. For our first example we’ll use Static.
Xcode will create a Swift file with some code to have you up and running. I made some changes in my example, but it is pretty similar to the template Xcode will give you for your project.
Let’s start with the UI. As I said a Widget must be build with SwiftUI and for our first example we’ll just show a couple of labels with text.
@main
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("Just a simple widget")
}
}
If you’re familiar with SwiftUI you can see that our Widget conform to the Widget protocol and has a body property that instead of returning some view, returns a WidgetConfiguration. If you don’t know what @main means check out this article.
Remember there a two configuration and for this example we chose the Static one, so we return a StaticConfiguration.
There are 3 parameters, kind is a string describing the widget (you can have more than one for an app), provider is an object conforming to Provider used to update the widget and the third parameter is a ViewBuilder that you can use to construct your SwiftUI view. There is a parameter called entry, Swift will inference its type based on your provider and we’ll get to that in a minute, but first let’s have a look at the actual UI
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text(entry.record.text)
}
}
}
This is a simple SwiftUI View with only one variable, the entry we saw before in the ViewBuilder.
At this time, you may wonder what is the type of entry. Where is Provider.Entry defined? You won’t find it in MyWidget.swift because Entry is an associated type defined the TimelineProvider protocol. This is the link to a good article from Paul Hudson on the subject if you want to understand what an associated type is.
TLDR Swift will figure out the type of entry based on the struct Provider. The only requirement is that the type of entry must conform to the protocol TimelineEntry.
TimelineProvider
I think it is time to talk about this Provider, conforming to the TimelineProvider protocol we keep referring to.
The first comment on Apple’s documentation is spot on: TimelineProvider is type that advises WidgetKit when to update a widget’s display.
The os will periodically request a timeline for a widget, a timeline is an array of entries with a Date and additional data that can be used to display a Widget.
I’ll show you how to ask the OS to update the Widget as soon as possible, but it not always necessary, you can create a timeline and have the widget be updated based on that array.
struct Provider: TimelineProvider {
let dataSource = DataSource()
func placeholder(in context: Context) -> MyEntry {
MyEntry(date:Date(), record: dataSource.getRecordToShow())
}
func getSnapshot(in context: Context, completion: @escaping (MyEntry) -> ()) {
let entry = MyEntry(date:Date(), record:dataSource.getRecordToShow())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [MyEntry] = []
let entry = MyEntry(date:Date(), record: dataSource.getRecordToShow())
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
This is the implementation of Provider.
The code you see above is not too different from the one provided you by Xcode. There are 3 functions you need to implement.
The first one is placeholder, a function that provides a single entry that doesn’t need to display meaningful data, as the name suggests this is just a placeholder.
Next we have getSnapshot, a function that provides a single entry representing the current state of your widget. There is a completion handler, where you’re supposed to provide an entry as soon as possible, the documentation suggest to provide sample data if you need to perform a long operation to fetch new data.
The third function is getTimeline and you can provide an array of entries instead of just one. Each entry has a date, so you can have your widget updated based on the date of each entry. For example, you may provide an entry per minute, or one every 30 seconds. When you create a Timeline, you can specify when you expect the os to call you again to get a new one in the policy parameter. There are 3 possible values: atEnd tells the system to ask for a new timeline after the last entry has expired, never means it is your responsibility to ask the os to update the timeline, after lets you specify a date when the os will call you for the new timeline.
A quick word on our entry. As I said before it must conform to the TimelineEntry protocol. Our implementation is very simple
struct MyEntry: TimelineEntry {
var date: Date
let record:Record
}
date is mandatory, and we can provide additional information in our Record struct.
The Widget part is done, everything is in MyWidget.swift, but we may want to display something useful coming from our app, let’s see how.
Update the Widget
Time to share some data between our simple iOS app and its Widget.
Remember a widget is an app extension and to share data between different targets we can use App Groups.
In order to use App Groups, you need to add the group to both targets. Select each target, go to Signing&Capabilities, press the + button and look for App Group. It will ask you a name, use the same name for both targets.
We can now update our app to send data to the widget.
func updateWidget(withText:String) {
let defaults = UserDefaults.init(suiteName: appGroupName)
defaults?.set(withText, forKey: "Text")
defaults?.synchronize()
WidgetCenter.shared.reloadAllTimelines()
}
An easy way to share data with App Groups, is using UserDefaults. In the example above appGroupName is the name I set in Signing & Capabilities, I can then set a key to the users defaults and synchronize it so the Widget can receive this information. Next, I call reloadAllTimelines so the system will force an update to the widget.
Let’s get back to our Widget for a moment. In the code, every time I provide a new entry for a timeline or a snapshot I get it from a DataSource object.
class DataSource {
private let appGroupName = "group.com.gfrigerio.WidgetTest"
func getRecordToShow() -> Record {
let defaults = UserDefaults.init(suiteName: appGroupName)
defaults?.synchronize()
let text = defaults?.value(forKey: "Text") as? String ?? "No value read"
return Record(color: .blue, text: text)
}
}
This piece of code runs on the Widget, reads the user defaults and returns a Record used to build a TimelineEntry.
Conclusion
This was a quick introduction to get you up to speed if you want to build a widget for your app.
There is more to say, like talking about multiple widgets for the same app, the Intent configuration and the ability to support multiple families (small, medium, large) but I think it will be better to have a separate article for those.
Happy coding 🙂