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. In ContentView, change the store initializer:
@StateObject private var store = TheMetStore(12)
You further reduce the number of objects returned, to reduce the number of calls to getObject(from:), because you’ll be making a lot of calls during this 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. Since 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… or by clicking + in the Targets section of the project window.
➤ Search for widget, select Widget Extension and click Next:
➤ Name it TheMetWidget and make sure Include Live Activity, Include Control 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.
New in iOS 18, a Control allows your app to execute an action, launch your app to a specific view, or launch a locked camera capture extension from Control Center, the Lock Screen or by using the Action button. Unlike the deep link you’ll implement in TheMet, a control works even when the app isn’t running in the background.
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 tutorials Getting Started With Widgets and Interactive Widgets With SwiftUI.
➤ 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
if #available(iOS 17.0, *) {
TheMetWidgetEntryView(entry: entry) // 3
.containerBackground(.fill.tertiary, for: .widget)
} else {
TheMetWidgetEntryView(entry: entry)
.padding()
.background()
}
}
// 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. iOS 17 allowed widgets in new locations on the Mac and iPad, andcontainerBackgroundenables you to define custom backgrounds for different contexts. - 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. Close the app (in a simulator, tap the Home button in the tool bar), then press on some empty area of the 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 Edit in the upper left corner, then select Add Widget. 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 or any empty area to turn off editing.
➤ 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. Between date and emoji, add the following line:
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 the widget target to Object.swift.
➤ In Project navigator, select Object.swift. Show the File inspector and, in the Target Membership box, click + to add 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
To fix the errors, 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)
Note: You might have to manually fix the final missing-argument error.
Then, in getSnapshot(in:completion:), change true to false so you’ll be able to see which one appears in the widget gallery. And also change one of the SimpleEntry instances in the preview’s timeline to false.
Note: If Xcode fails 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’”, do this: Change
Timeline<Entry>toTimeline<SimpleEntry>in thegetTimeline(in:completion:)signature in TheMetWidget.swift. This usually fixes the problem. Thereafter, if the preview crashes, press Shift-Option-Command-K to clean the build folder immediately, then refresh the preview.
Creating Widget Views
When you’ve decided what data to display, you need to modify TheMetWidgetEntryView 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.
➤ In TheMetWidgetEntryView, replace the VStack with this:
Text(entry.object.title)
.font(.title3)
.lineLimit(3)
You’ll display the object’s title in a large-ish font and display up to three lines of text.
This is a good time to delete the emoji declaration in SimpleEntry and delete its argument in all SimpleEntry instances TheMetWidget.swift:
// delete , emoji: "😀"
SimpleEntry(date: Date(), object: Object.sample(isPublicDomain: true, emoji: "😀"))
➤ Refresh the preview to see what you’ve got:
To preview a widget, you supply timeline entries, which you can click through, forward or back, or play — on repeat, the timeline updates about every three seconds.
Using App Views & Assets
Now, to make it look more like the app’s list view, you’ll need the WebIndicatorView from ContentView.swift and the metBackground and metForeground colors defined in Assets.
➤ In TheMet folder, add the widget target to Assets.xcassets.
Note: Adding the widget target to ColorExtension.swift causes duplicate declaration errors.
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 folder, add a new SwiftUI View file named SupportingViews.swift, with both targets:
➤ Delete the struct and the preview, then move WebIndicatorView from ContentView.swift and PlaceholderView from ObjectView.swift into your new file.
➤ Back in TheMetWidget.swift, add this structure above TheMetWidgetEntryView:
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 TheMetWidgetEntryView, replace the contents of body with the following:
VStack {
Text("The Met") // 1
.font(.headline)
Divider() // 2
if !entry.object.isPublicDomain { // 3
WebIndicatorView(title: entry.object.title)
.padding()
.background(.metBackground)
.foregroundStyle(.white)
} else {
DetailIndicatorView(title: entry.object.title)
.padding()
.background(.metForeground)
}
}
.truncationMode(.middle) // 4
.fontWeight(.semibold)
Note: ColorExtension.swift isn’t in the widget target, so its color aliases shouldn’t work here, but they do. Even more surprisingly, the more-correct
Color("met-Foreground")andColor("met-Background")don’t work.
Here’s what you’re doing:
- You can’t use
NavigationStackin a widget view, so you create your own title withheadlinefont size. - 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.
➤ The text and indicators don’t fit in the small widget view, so change the preview to show the medium size:
#Preview(as: .systemMedium) {
➤ Refresh the preview to see your improved widget:
➤ To see how the large 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 TheMetWidget.swift to change .systemMedium to .systemLarge in the preview:
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])
➤ 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:).
➤ Remove the widget.
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.
➤ First, add the widget target to TheMetService.swift, TheMetStore.swift and URLComponentsExtension.swift in TheMet folder.
➤ 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.
➤ Now, in getTimeline(in:completion:), replace the for loop with the following code:
let interval = 2
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, two seconds apart. IffetchObjects(for:)fails, you fill the array with the two sample objects. - You change the interval between entries to
intervalseconds. - 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. You can also refresh the preview so you have another place to view the list of persimmon objects.
➤ You need to run this on your device, so change the bundle identifiers and set the team for each target, if you haven’t already done this.
➤ Check the scheme is TheMet, then build and run on your device, wait while it loads the objects, then close the app. Look for your widget and add it. Then🤞and 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 several seconds, tap the widget to open the app, then close it again. 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.
➤ 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 gray or 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, near the top:
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. If it’s stuck on one object, delete the widget, add it again, tap into your app, set another query, then close the app. Sometimes it helps to delete the app from your device, then reinstall it.
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
➤ Remove the widget from your device.
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 TheMetWidget.swift, in TheMetWidgetEntryView, 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 Model, Networking and Views folders:
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.