Getting Started With Widgets
In this tutorial, you’ll add a widget to a large SwiftUI app, reusing its views to show entries from the app’s repository. By Audrey Tam.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With Widgets
25 mins
- Getting Started
- WidgetKit
- Adding a Widget Extension
- Running Your Widget
- Defining Your Widget
- Creating a TimelineEntry
- Creating an Entry View
- Creating Your Widget
- Creating a Snapshot Entry
- Creating a Temporary Timeline
- Defining Your Widget
- Providing Timeline Entries
- Creating an App Group
- Writing the Contents File
- Reading the Contents File
- Enabling User Customization
- Adding an Intent
- Reconfiguring Your Widget
- Where To Go From Here?
Defining Your Widget
It makes sense to make your widget display some of the information the app shows for each tutorial.
This view is defined in UI/Shared/Content List/CardView.swift. My first idea was to just add the widget target to this file. But that required adding more and more and more files, to accommodate all the intricate connections in Emitron.
All you really need are the Text
views. The images are cute, but you’d need to include the persistence infrastructure to keep them from disappearing.
You’re going to copy the layout of the relevant Text
views. These use several utility extensions, so find these files and add the EmitronWidgetExtension target to them:
CardView displays properties of a ContentListDisplayable
object. This is a protocol defined in Displayable/ContentDisplayable.swift:
protocol ContentListDisplayable: Ownable {
var id: Int { get }
var name: String { get }
var cardViewSubtitle: String { get }
var descriptionPlainText: String { get }
var releasedAt: Date { get }
var duration: Int { get }
var releasedAtDateTimeString: String { get }
var parentName: String? { get }
var contentType: ContentType { get }
var cardArtworkUrl: URL? { get }
var ordinal: Int? { get }
var technologyTripleString: String { get }
var contentSummaryMetadataString: String { get }
var contributorString: String { get }
// Probably only populated for screencasts
var videoIdentifier: Int? { get }
}
Your widget only needs name
, cardViewSubtitle
, descriptionPlainText
and releasedAtDateTimeString
. So you’ll create a struct for these properties.
Creating a TimelineEntry
Create a new Swift file named WidgetContent.swift and make sure its targets are emitron and EmitronWidgetExtension:
It should be in the EmitronWidget group.
Now add this code to your new file:
import WidgetKit
struct WidgetContent: TimelineEntry {
var date = Date()
let name: String
let cardViewSubtitle: String
let descriptionPlainText: String
let releasedAtDateTimeString: String
}
To use WidgetContent
in a widget, it must conform to TimelineEntry
. The only required property is date
, which you initialize to the current date.
Creating an Entry View
Next, create a view to display the four String
properties. Create a new SwiftUI View file and name it EntryView.swift. Make sure its target is only EmitronWidgetExtension, and it should also be in the EmitronWidget group:
Now replace the contents of struct EntryView
with this code:
let model: WidgetContent
var body: some View {
VStack(alignment: .leading) {
Text(model.name)
.font(.uiTitle4)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
.padding([.trailing], 15)
.foregroundColor(.titleText)
Text(model.cardViewSubtitle)
.font(.uiCaption)
.lineLimit(nil)
.foregroundColor(.contentText)
Text(model.descriptionPlainText)
.font(.uiCaption)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
.lineSpacing(3)
.foregroundColor(.contentText)
Text(model.releasedAtDateTimeString)
.font(.uiCaption)
.lineLimit(1)
.foregroundColor(.contentText)
}
.background(Color.cardBackground)
.padding()
.cornerRadius(6)
}
You’re essentially copying the Text
views from CardView
and adding padding.
Delete EntryView_Previews
entirely.
Creating Your Widget
Now start defining your widget. Open EmitronWidget.swift and double-click SimpleEntry
in the line:
struct SimpleEntry: TimelineEntry {
Choose Editor ▸ Edit All in Scope and change the name to WidgetContent. This will cause several errors, which you’ll fix in the next few steps. First delete the declaration:
struct WidgetContent: TimelineEntry {
let date: Date
}
This declaration is now redundant and conflicts with the one in WidgetContent.swift.
One of the provider’s methods provides a snapshot entry to display in the widget gallery. You’ll use a specific WidgetContent
object for this.
Just below the import
statements, add this global object:
let snapshotEntry = WidgetContent(
name: "iOS Concurrency with GCD and Operations",
cardViewSubtitle: "iOS & Swift",
descriptionPlainText: """
Learn how to add concurrency to your apps! \
Keep your app's UI responsive to give your \
users a great user experience.
""",
releasedAtDateTimeString: "Jun 23 2020 • Video Course (3 hrs, 21 mins)")
This is the update to our concurrency video course, which was published on WWDC day 2.
Now replace the line in placeholder(in:)
with this:
snapshotEntry
To display your widget for the first time, WidgetKit renders this entry in the widget’s view, using the modifier redacted(reason: .placeholder)
. This displays text and images in the correct layout, but masks their contents. This method is synchronous, so don’t do any network downloads or complex calculations here.
Also replace the first line of getSnapshot(in:completion:)
with this:
let entry = snapshotEntry
WidgetKit displays this entry whenever the widget is in a transient state, waiting for data or appearing in the widget gallery.
A widget needs a TimelineProvider
to feed it entries of type TimelineEntry
. It displays each entry at the time specified by the entry’s date
property.
The most important provider method is getTimeline(in:completion:)
. It already has some code to construct a timeline, but you don’t have enough entries yet. So comment out all but the last two lines, and add this line above those two lines:
let entries = [snapshotEntry]
You’re creating an entries
array that contains just your snapshotEntry
.
Finally, you can put all these parts together.
First, delete EmitronWidgetEntryView
. You’ll use your EntryView
instead.
Now replace the internals of struct EmitronWidget
with the following:
private let kind: String = "EmitronWidget"
public var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: Provider()
) { entry in
EntryView(model: entry)
}
.configurationDisplayName("RW Tutorials")
.description("See the latest video tutorials.")
}
The three strings are whatever you want: kind
describes your widget, and the last two strings appear above each widget size in the gallery.
Build and run on your device, sign in, then close the app to see your widget.
If it’s still displaying the time, delete it and add it again.
And here’s what the medium size widget looks like now:
Only the medium size widget looks OK, so modify your widget to provide only that size. Add this modifier below .description
:
.supportedFamilies([.systemMedium])
Next, you’ll provide real entries for your timeline, directly from the app’s repository!