25.
Widgets
Written by Audrey Tam
Ever since Apple showed off its new home screen widgets in the 2020 WWDC Platforms State of the Union, everyone has been creating them. It’s definitely a useful addition to TheMet, providing convenient and quick access to objects listed in your app.
Note: The WidgetKit API continues to evolve at the moment, which may result in changes that break your code. Apple’s template code has changed a few times since the 2020 WWDC demos. You might still experience some instability. That said, Widgets are cool and a ton of fun!
Getting Started
▸ Open the starter project or continue with your app from the previous chapter.
WidgetKit
WidgetKit is Apple’s API for adding widgets to your app. The widget extension template helps you create a timeline of entries. You decide what app data you want to display and the time interval between entries.
You also define a view for each size of widget — small, medium, large, extra large — you want to support. The extra large size is available only in iPadOS.
Here’s a typical workflow for creating a widget:
-
Add a widget extension to your app. Configure the widget’s display name and description.
-
Select or adapt a data model type from your app to display in the widget. Create a timeline entry structure — a
Dateplus your data model type. Create sample data for snapshot and placeholder entries. -
Decide which widget sizes to support: Create small, medium or large views to display one or more data model values. In iOS 16, you can also create accessory views to display on the device’s lock screen.
-
Create a timeline to deliver timeline entries. Decide on the refresh policy.
Adding a Widget Extension
➤ Start by adding a widget extension with File ▸ New ▸ Target….
➤ Search for widget, select Widget Extension and click Next:
➤ Name it TheMetWidget, select your team and make sure Include Live Activity and Include Configuration Intent are not checked:
A Live Activity display shows an app’s most current data on the iPhone Lock Screen and in the Dynamic Island. This chapter doesn’t implement Live Activity.
There are two widget configurations: Static and Intent. A widget with IntentConfiguration uses Siri intents to let the user customize widget parameters. Your widget for TheMet will be static.
Note:
IntentConfigurationis covered in our tutorial Getting Started With Widgets
➤ Click Finish and agree to the activate-scheme dialog:
Configuring Your Widget
A new target group named TheMetWidget appears in the Project navigator. It contains two Swift files.
TheMetWidgetBundle.swift is like TheMetApp.swift, but it instantiates your widget instead of the first view of your app. The @main attribute means this is the widget’s entry point.
@main
struct TheMetWidgetBundle: WidgetBundle {
var body: some Widget {
TheMetWidget()
}
}
➤ Open TheMetWidget.swift, then find TheMetWidget and edit the last two modifiers: configurationDisplayName(_:) and description(_:).
struct TheMetWidget: Widget { // 1
let kind: String = "TheMetWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: Provider() // 2
) { entry in
TheMetWidgetEntryView(entry: entry) // 3
}
// 4
.configurationDisplayName("The Met")
.description("View objects from the Metropolitan Museum.")
}
}
Here’s what the template code does:
- The structure’s name and its
kindproperty are the name you gave it when you created it. - You define your widget’s timeline, snapshot and placeholder entries in
Provider. - You create your widget view(s) in
TheMetWidgetEntryView. - In this structure, you only need to customize the name to The Met and the description to View objects from the Metropolitan Museum. Your users will see these in the widget gallery.
Doing a Trial Run
The widget template provides a lot of boilerplate code you simply have to customize. It works right out of the box, so you can try it out now to make sure everything runs smoothly when you’re ready to test your code.
➤ You can try out your widget in a simulator. If you want to install your app on your iOS device, you need to sign both targets. In the Project navigator, select the top level TheMet folder. Use your organization instead of “com.yourcompany” in the bundle identifiers and set the team for each target.
Note: Your widget’s bundle ID prefix must be the same as your app’s. This isn’t a problem with TheMet but, if your project has different bundle IDs for Debug, Release and Beta, you’ll need to edit your widget’s bundle ID prefix to match.
➤ TheMetWidget is a second target, and it’s probably the currently selected scheme. Make sure you select the TheMet scheme, then build and run. Tap the Home button in the simulator tool bar to close the app, then press on some empty area of your home window until you see delete buttons on the app icons.
Note: The app’s scheme TheMet might disappear from the scheme menu. If this happens, select Manage Schemes… from the menu, then click Autocreate Schemes Now:
➤ Tap + in the upper left corner. If you’ve installed the app on a device, your gallery looks something like this:
➤ The quickest way to find your widget is to start typing TheMet in the Search Widgets field:
➤ Select TheMet to see snapshots of the three sizes:
➤ Tap Add Widget to see your widget on the screen:
➤ Tap Done in the upper right corner.
➤ Tap the widget to reopen TheMet.
Your widget works! Now, you simply have to make it display information from TheMet.
➤ Close the app then long-press the widget to open its menu and select Remove Widget. Get into the habit of removing the widget after you’ve confirmed it’s working. This is especially important if you’ve installed the app on your device. While you’re developing your widget, it will display a new view every three seconds, and that’s a real drain on your battery.
Creating Entries From Your App’s Data
It makes sense for your widget to display some of the information your app shows for each object, using the properties in Object.swift.
Adding App Files to the Widget Target
➤ In TheMetWidget.swift, find SimpleEntry. Add this line below date:
let object: Object
Your widget will display an object from the app’s list.
An Xcode error appears, because the widget doesn’t know about Object. You need to add Object.swift to the widget target.
➤ In Project navigator, select Object.swift. Show the File inspector and check the Target Membership box for TheMetWidgetExtension:
Provider Methods
Adding the object property to SimpleEntry causes errors in Provider because it creates SimpleEntry instances in its methods placeholder(in:), getSnapshot(in:completion:), getTimeline(in:completion:) and also in the preview. Provider methods are called by WidgetKit, not by any code you write.
- To display your widget for the first time, WidgetKit calls
placeholder(in:)and applies aredacted(reason: .placeholder)modifier to mask the view’s contents. This method is synchronous: Nothing else can run on its queue until it finishes, so don’t do any network downloads or complex calculations in this method. - WidgetKit calls
getSnapshot(in:completion:)whenever the widget is in a transient state, waiting for data or appearing in the widget gallery. - WidgetKit calls
getTimeline(in:completion:)to get an array of time-stamped entries to display.
Creating Sample Objects
First, you need a sample Object for the parameter value.
➤ In Object.swift, add this extension to Object:
extension Object {
static func sample(isPublicDomain: Bool) -> Object {
if isPublicDomain {
return Object(
objectID: 452174,
title: "Bahram Gur Slays the Rhino-Wolf",
creditLine: "Gift of Arthur A. Houghton Jr., 1970",
objectURL: "https://www.metmuseum.org/art/collection/search/452174",
isPublicDomain: true,
primaryImageSmall: "https://images.metmuseum.org/CRDImages/is/original/DP107178.jpg")
} else {
return Object(
objectID: 828444,
title: "Hexagonal flower vase",
creditLine: "Gift of Samuel and Gabrielle Lurie, 2019",
objectURL: "https://www.metmuseum.org/art/collection/search/828444",
isPublicDomain: false,
primaryImageSmall: "")
}
}
}
This method returns a sample object, either in the public domain or not. The method is static, so you can call it with Object.sample(isPublicDomain: true).
➤ Now, in TheMetWidget.swift, fix the errors one by one, or use the handy shortcut Control-Option-Command-F for Editor ▸ Fix All Issues to insert the missing argument. Replace all the Object placeholders in TheMetWidget.swift with:
Object.sample(isPublicDomain: true)
Then, in getSnapshot(in:completion:), change true to false so you’ll be able to see which one appears in the widget gallery.
Creating Widget Views
When you’ve decided what data to display, you need to define a widget view to display it. It would be nice to display the primary image of an object in your widget view, but AsyncImage(url:) doesn’t work in a widget, so you’ll simply display the object’s title.
➤ Create a new SwiftUI View file named WidgetView.swift, making sure you set TheMetWidgetExtension as its target:
➤ Replace the content of WidgetView with this:
let entry: Provider.Entry
var body: some View {
Text(entry.object.title)
}
The widget needs an Entry to display and, to start, you’ll display the object’s title.
➤ Now, fix the preview: Copy the content of TheMetWidget_Previews from TheMetWidget.swift, then change TheMetWidgetEntryView to WidgetView:
WidgetView(
entry: SimpleEntry(
date: Date(),
object: Object.sample(isPublicDomain: true)))
.previewContext(WidgetPreviewContext(family: .systemSmall))
➤ To use WidgetPreviewContext, you need to import WidgetKit:
import WidgetKit
➤ Refresh the preview to see what you’ve got:
Note: Xcode might fail to build, complaining that “Embedded binary is not signed with the same certificate as the parent app” or “Reference to invalid associated type ‘Entry’ of type ‘Provider’”. Changing
Timeline<Entry>toTimeline<SimpleEntry>in thegetTimeline(in:completion:)signature in TheMetWidget.swift can get rid of this problem.
It works! Now, to make it look more like the app’s list view, you’ll need the WebIndicatorView from ContentView.swift and the metBackground color defined in Assets and ColorExtension.swift.
➤ From TheMet group, add Assets.xcassets and ColorExtension.swift to the widget target.
Instead of adding the whole ContentView.swift to the widget target, you’ll move WebIndicatorView to a separate SwiftUI View file, along with PlaceholderView.
➤ In TheMet group, add a new SwiftUI View file named SupportingViews.swift, delete its structures, then move WebIndicatorView from ContentView.swift and PlaceholderView from ObjectView.swift into your new file. Add SupportingViews.swift to the widget target.
➤ Back in WidgetView.swift, add this structure:
struct DetailIndicatorView: View {
let title: String
var body: some View {
HStack(alignment: .firstTextBaseline) {
Text(title)
Spacer()
Image(systemName: "doc.text.image.fill")
}
}
}
The app displays a detail view for public-domain objects, with some text and an image. By the end of this chapter, you’ll implement a deep-link to the object’s detail view, so here you include a little system image to suggest what the user will see.
➤ Now, in WidgetView, replace Text(entry.object.title) with the following:
VStack {
Text("The Met") // 1
.font(.headline)
.padding(.top)
Divider() // 2
if !entry.object.isPublicDomain { // 3
WebIndicatorView(title: entry.object.title)
.padding()
.background(Color.metBackground)
.foregroundColor(.white)
} else {
DetailIndicatorView(title: entry.object.title)
.padding()
.background(Color.metForeground)
}
}
.truncationMode(.middle) // 4
.fontWeight(.semibold)
Here’s what you’re doing:
- You can’t use
NavigationStackin a widget view, so you create your own title withheadlinefont size andtoppadding to push it away from the top edge. - You add a divider line, to make it look more like a title.
- You display the object’s title so it looks similar to how it appears in the app’s list.
- You apply
truncationModeandfontWeightto theVStackso it works for bothWebIndicatorViewandDetailIndicatorView.
➤ Refresh the preview to see your improved widget:
A Group of Previews
You can preview both sample objects by creating a Group:
➤ In WidgetView_Previews, replace the contents of previews with:
Group {
WidgetView(
entry: SimpleEntry(
date: Date(),
object: Object.sample(isPublicDomain: true)))
.previewContext(WidgetPreviewContext(family: .systemSmall))
// non-public-domain sample object
WidgetView(
entry: SimpleEntry(
date: Date(),
object: Object.sample(isPublicDomain: false)))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
This embeds WidgetView in a Group, duplicates it and its .previewContext modifier, and changes true to false.
➤ In the preview canvas, select the second Widget View to see the WebIndicator view:
➤ Now, in the second WidgetView, change .systemSmall to .systemMedium:
This is how your WidgetView layout looks in the medium size widget.
➤ To see how the medium size might be useful, go to Object.swift and replace the title of the public-domain object:
title: "\"Bahram Gur Slays the Rhino-Wolf\", Folio 586r from the Shahnama (Book of Kings) of Shah Tahmasp",
This is the full title that the app downloads.
➤ And back to WidgetView.swift to change .systemSmall to .systemMedium in the first WidgetView:
➤ Finally, in the first WidgetView, change .systemMedium to .systemLarge:
Supporting Widget Sizes
If you think one of the sizes looks best, or if you definitely don’t want to support one of the sizes, you can restrict your widget to specific size(s). For TheMet, long titles look better in the medium or large size.
➤ In TheMetWidget, add this modifier to StaticConfiguration, below description(_:):
.supportedFamilies([.systemMedium, .systemLarge])
Finally, you need to set up TheMetWidgetEntryView to use WidgetView. Replace the body contents with this code:
WidgetView(entry: entry)
Here, you set WidgetView as the view to use when you want to display content.
➤ And in previews, change .systemSmall to .systemMedium or .systemLarge:
.previewContext(WidgetPreviewContext(family: .systemMedium))
➤ Make sure the scheme is TheMet, then build and run the app. After it launches, close the app.
If you had a small widget installed before this, it’s now gone. And, when you add a widget, the small size isn’t an option:
Notice the gallery uses the non-public-domain sample object, which means it’s calling getSnapshot(in:completion:).
Note: If your widget doesn’t appear in the gallery, or doesn’t work correctly, delete the app then build and run again. If the problem persists, restart the simulator or device.
➤ Add a widget, then tap the screen or the Done button to exit screen-editing mode.
The widget view displays the SimpleEntry you set up in getTimeline(in:completion:).
Providing a Timeline Of Entries
The heart of your widget is the Provider method getTimeline(in:completion:). It delivers an array of time-stamped entries for WidgetKit to display. The template code creates an array of five entries one hour apart:
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(
byAdding: .hour,
value: hourOffset,
to: currentDate)!
let entry = SimpleEntry(
date: entryDate,
object: Object.sample(isPublicDomain: true))
entries.append(entry)
}
This code creates each entry with the same Object.sample. You’ll modify the method so it displays items in the app’s objects array. Waiting an hour between entries is no good for testing purposes, so you’ll shorten the interval to a few seconds.
First, you must populate your objects array.
Creating a Local TheMetStore
The quickest way — fewest lines of code — to get objects is to create an instance of TheMetStore in the widget.
➤ In TheMetWidget.swift, add these properties to Provider:
let store = TheMetStore(6)
let query = "persimmon"
While debugging, you limit the number of downloaded objects to a small number. You set the query term to something that returns objects with distinct titles.
➤ To get rid of the error flags, add TheMetStore.swift, TheMetService.swift and URLComponentsExtension.swift from TheMet group to the widget target.
➤ Now, in getTimeline(in:completion:), replace the for loop with the following code:
let interval = 3
Task { // 1
do {
try await store.fetchObjects(for: query)
} catch {
store.objects = [
Object.sample(isPublicDomain: true),
Object.sample(isPublicDomain: false)
]
}
}
for index in 0 ..< store.objects.count {
let entryDate = Calendar.current.date(
byAdding: .second, // 2
value: index * interval,
to: currentDate)!
let entry = SimpleEntry(
date: entryDate,
object: store.objects[index]) // 3
entries.append(entry)
}
- You call
fetchObjects(for:)to fill theobjectsarray and use this to create an array ofSimpleEntryvalues, three seconds apart. IffetchObjects(for:)fails, you fill the array with the two sample objects. - You change the interval between entries to 3 seconds.
- You display an object from
store.objects.
➤ At the top of ContentView, change query to “persimmon”: You download the same objects as the widget, so you can compare the app’s list with what your widget displays.
➤ Build and run, then close the app. Look for your widget and add it. Then watch it display the first six persimmon objects:
Note: If you already had a Widget added in the home screen and it isn’t showing the objects, remove it and add it again.
The widget might take a while to start displaying. In the meantime, it displays the placeholder view. If nothing happens after a couple of minutes, build and run the app again. After the sixth object, there’s a longer interval while the widget re-fetches the same six objects.
Note: At the time of writing, the widget doesn’t work correctly on my iPhone. It displays the first object, but doesn’t update.
➤ Tap the widget to reopen your app. Set a new query term, wait for the list to reload, then close the app. Your widget is still displaying persimmon objects:
Your widget’s TheMetStore is separate from your app’s TheMetStore, so it’s still using persimmon as the query term. You need to decide between these two design options:
- Keep the widget’s array in sync with the app’s.
- Allow the user to set a different query term for the widget.
Later in this chapter, you’ll implement a deep link from the widget into your app to open the detail view of the widget’s entry. This won’t make sense if the widget’s array could be different from the app’s array. So this chapter chooses the first design option.
Note: The second option requires you to create a widget with an
IntentConfiguration, covered in our tutorial Getting Started With Widgets.
Creating an App Group
Xcode Tip: App group containers allow apps and targets to share resources.
Whenever the user changes the query term in your app, fetchObjects(for:) downloads and decodes a new objects array. To share this array with your widget, you’ll create an app group. Then, in TheMetStore.swift, you’ll write a file to this app group, which you’ll read from in TheMetWidget.swift.
➤ If you haven’t signed the targets yet, do it now: In the Project navigator, select the top level TheMet folder. For each target, change the bundle identifier prefix to your organization instead of com.yourcompany and set the team.
➤ Now select the TheMet target. In the Signing & Capabilities tab, click + Capability, then drag App Groups into the window. Click + to add a new container.
➤ Name your container group.your.prefix.TheMet.objects. Be sure to replace your.prefix with your bundle identifier prefix. Click the reload button if the color of your group doesn’t change from red to black.
➤ Now select the TheMetWidgetExtension target and add the App Groups capability. If necessary, scroll through the App Groups to find and select group.your.prefix.TheMet.objects.
Reloading the Widget’s Timeline
Next, you’ll set up TheMetStore so it tells the widget to reload its timeline whenever fetchObjects(for:) finishes downloading and decoding an array of objects.
➤ Back in TheMetStore.swift, add this import statement:
import WidgetKit
fetchObjects(for:) needs to call a WidgetCenter method to reload your widget’s timeline.
➤ In fetchObjects(for:), add this line after the for-loop:
WidgetCenter.shared.reloadTimelines(ofKind: "TheMetWidget")
When the navigation stack in ContentView first loads, it calls fetchObjects(for:) to create the objects array, but this is an asynchronous task, so a user might install the widget while its objects array is empty. You tell the widget to reload its timeline when the array is ready.
Writing the App Group File
➤ At the top of TheMetStore.swift, just below the import WidgetKit statement, add this code:
extension FileManager {
static func sharedContainerURL() -> URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier:
"group.your.prefix.TheMet.objects")!
}
}
This is simply some standard code for getting the app group container’s URL. Be sure to substitute your bundle identifier prefix.
It makes sense to write this app group file just after you’ve decoded the data into the objects array. To write an array to a file, you JSON-encode it. Then the widget JSON-decodes the file contents.
➤ To write the file, add this helper method to TheMetStore:
func writeObjects() {
let archiveURL = FileManager.sharedContainerURL()
.appendingPathComponent("objects.json")
print(">>> \(archiveURL)")
if let dataToSave = try? JSONEncoder().encode(objects) {
do {
try dataToSave.write(to: archiveURL)
} catch {
print("Error: Can't write objects")
}
}
}
Here, you convert your array of Object values to JSON and save it to the app group’s container.
➤ In fetchObjects(for:), add the following to call your new helper method before the call to WidgetCenter:
writeObjects()
The existing call to WidgetCenter now tells the widget to reload its timeline whenever your app has written a new array of objects into a file in the app group.
Next, go and set up the widget to read this file.
Reading the Objects File
➤ Open TheMetWidget.swift.
Now, you can read your objects array from the app group file.
➤ Add this helper method to Provider:
func readObjects() -> [Object] {
var objects: [Object] = []
let archiveURL =
FileManager.sharedContainerURL()
.appendingPathComponent("objects.json")
print(">>> \(archiveURL)")
if let codeData = try? Data(contentsOf: archiveURL) {
do {
objects = try JSONDecoder()
.decode([Object].self, from: codeData)
} catch {
print("Error: Can't decode contents")
}
}
return objects
}
This reads the Object values from the file that fetchObjects(for:) saved into the app group’s container.
➤ Delete these lines from Provider:
let store = TheMetStore(6)
let query = "persimmon"
You won’t be using a local TheMetStore anymore.
➤ Also delete the Task in getTimeline(in:completion:) that calls fetchObjects(for:). Then, in getTimeline(in:completion:), replace the for loop with the following code:
let objects = readObjects()
for index in 0 ..< objects.count {
let entryDate = Calendar.current.date(
byAdding: .second,
value: index * interval,
to: currentDate)!
let entry = SimpleEntry(
date: entryDate,
object: objects[index])
entries.append(entry)
}
You read the objects array from the app group file and use it instead of store.objects to create entries.
Note: If you changed the bundle identifier, you’ll end up having two apps. Delete the old one before running the project.
➤ Build and run, then close the app. Look for your widget and add it. Watch it display a few persimmon objects, then tap the widget to reopen your app. Set query to giraffe, wait for the list to reload, then close the app. After a while, your widget will start displaying giraffe objects:
Note: If the widget keeps displaying persimmon objects, tap the widget or the app’s icon to reopen the app, then close the app again.
Your widget is working well, and you could happily install it on your device now. If you want to do so, skip down to the end of this chapter to change the timeline back to one-hour intervals.
The next section adds a feature many users expect: When you tap the widget, the app should open in the ObjectView for the current widget entry, if it’s public domain. Tapping a non-public-domain object should open the metmuseum.org page for the object.
Deep-Linking Into Your App
You can set up your widget with a deep link to activate a NavigationLink that opens the ObjectView or SafariView of the widget entry object. Here’s your workflow:
- Create a URL scheme.
- Modify a suitable view in
WidgetViewwithwidgetURL(_:). - In your app, implement
onOpenURL(perform:)to activate the correct.navigationDestinationmodifier.
Note: When you install the app on a device, deep-linking works when the app is running in the background. If the app isn’t running at all, tapping the widget opens the app and shows the list.
Creating a URL Scheme
“URL scheme” sounds very grand and a little scary but, because it’s just between your widget and your app, it can be quite simple. You’re basically creating a tiny API between widget and app. The widget needs to send enough information to the app so the app knows which view to display. Formatting this information as a URL lets you use URL or URLComponents properties to extract the necessary values.
For this app, the objectID property of Object uniquely identifies it. So the URL to open “Hexagonal flower vase” is simply:
URL(string: "TheMet://828444")
And you can access this objectID value as the host property of the URL. So simple!
In Your Widget
➤ In WidgetView.swift, add this modifier to the top-level VStack, where you set truncationMode and fontWeight:
.widgetURL(URL(string: "themet://\(entry.object.objectID)"))
Note: In the medium and large widget sizes, you can use
Link(_:destination:)to attach links to different parts of the view.
In Your App
In your app, you implement .onOpenURL(perform:) to process the widget URL. You attach this modifier to either the root view, in TheMetApp, or to the top level view of the root view. For TheMet, you’ll attach this to the NavigationStack in ContentView, because the perform closure must assign a value to a @State property of ContentView.
You need to trigger navigation programmatically: You’ll add the widget’s object to a navigation path to make the app open the correct navigation destination.
➤ In ContentView.swift, add this @State property to ContentView:
@State private var path = NavigationPath()
When the widget sends a widgetURL to the app, you’ll check whether the object is in the public domain or not. Then, you’ll append either the object or its URL to path, and NavigationStack will use this to select the correct navigationDestination.
Note: If your
NavigationStackpresents only one type of view,pathcan be an array of the data type you pass to that view:[Object]forObjectViewor[URL]forSafariView. You’ll still need to useNavigationLink(value:)with the.navigationDestination(for:)modifier.
➤ To use this path, replace NavigationStack { with this:
NavigationStack(path: $path) {
You pass a binding to path to the navigation stack. Now, you can observe the current state of the stack or modify path to specify where to navigate.
➤ Now, add this modifier to NavigationStack, at the same level as the task that calls fetchObjects(for:):
.onOpenURL { url in
if let id = url.host,
let object = store.objects.first(
where: { String($0.objectID) == id }) { // 1
if object.isPublicDomain { // 2
path.append(object)
} else {
if let url = URL(string: object.objectURL) {
path.append(url)
}
}
}
}
Here’s what this does:
- Extract an
idvalue from the widget URL, then find the firstobjectwhoseobjectIDmatches thisidvalue. Becauseurl.hostis aString, convert theobjectIDvalue toStringbefore comparing. - If the object is in the public domain, append it to
path. Otherwise, append theURLcreated from itsobjectURL.
➤ At the top of ContentView, change query to peony: This query returns more non-public-domain objects, so you’ll be able to test that tapping these objects opens the app in SafariView.
➤ Build and run, wait for the list to load, then close the app and add your widget. Tap a public-domain entry to see it open the ObjectView for that object:
➤ Tap the app’s back button to return to the list, then close the app and tap a non-public-domain entry to see it open the SafariView for that object:
Well done!
A Few Last Things
A couple of housekeeping items before you go.
Organizing TheMet Group
➤ Organize your app files by grouping them into Views, Model and Networking:
Using Normal Timing
You’ve been using a three-second interval in your timeline to make testing simpler. You definitely don’t want to release your widget with such a short interval. If you want to use TheMet on your device as a real app, set up the timeline to change every hour instead of every three seconds.
Note: The project in the final folder still displays every three seconds.
➤ In TheMetWidget.swift, in the for-loop of getTimeline(in:completion:), change the entryDate code to this:
let entryDate = Calendar.current.date(
byAdding: .hour,
value: index,
to: currentDate)!
You’re restoring the template code’s original timing. Now, your widget will add entries one hour apart from each other. You can add it to your device’s home screen with no worries about excessive battery use.
➤ Also remove the declaration of interval, as Xcode so helpfully suggests, since you’re no longer using it.
Refresh Policy
In getTimeline(in:completion:), after the for loop, you create a Timeline(entries:policy:) instance. The template sets policy to .atEnd, so WidgetKit creates a new timeline after the last date in the current timeline. As you saw when the widget was downloading a small number of its own objects, the new timeline doesn’t start immediately.
Of course, your current timeline fires at 3-second intervals, which is far from normal. With a more normal interval, like one hour, you probably won’t notice any delay.
There are two other TimelineReloadPolicy options:
-
after(_:): Specify aDatewhen you want WidgetKit to refresh the timeline. LikeatEnd, this is more a suggestion to WidgetKit than a hard deadline. -
never: Use this policy if your app usesWidgetCenterto tell WidgetKit when to reload the timeline. This is a good option for TheMet. You’ve already seen the timeline reload almost immediately when you change a query option in your app. You could add code to your app to callfetchObjects(for:)at the same time every day, and this would also refresh your widget’s timeline.
Key Points
- WidgetKit is still a relatively new API. You might experience some instability. You can fix many problems by deleting the app or by restarting the simulator or device.
- To add a widget to your app, decide what app data you want to display and the time interval between entries. Then, define a view for each size of widget — small, medium, large — you want to support.
- Add app files to the widget target and adapt your app’s data structures and views to fit your widgets.
- Create an app group to share data between your app and your widget.
- Deep-linking from your widget into your app is easy to do.