Intent definitions are used to make interactions with app extensions configurable. Most primarily they can be used to configure your widgets on the home or lock screen. They also can be used to configure widgets on watchOS. Starting with the latest OS version updates (iOS 16, macOS 13, watchOS 9) the IntentTimelineProvider
was enhanced to enable developers to provide default configured widgets on platforms that do not offer a native way of configuring widgets. In this blog post, I want to highlight how you can provide pre-configured widgets to your users as well as highlight a very weird bug I encountered at my day job last week.
Intent definitions
Intent definition files can be used to let your users configure widgets to their needs. On iOS or iPadOS the system per default offers a UI based on the intent definition for the user to configure the widget accordingly.
This is very neat since we do not need to take care of that UI. iOS is doing the heavy lifting for us. But there are also platforms where this native UI is not available. On these platforms, IntentTimelineProvider.recommendations()
(Apple Documentation) comes into play.
Recommendations
Starting with the latest OS versions, the IntentTimelineProvider
added a new method to provide intent configuration recommendations. You can think of these recommendations as a programmatic version of one set of configurations for your widget. For example when we have a look at the watchOS complications selection screen. You can think of these available selections as recommendations. Each of them can either represent a static widget or a configurable one with a configuration preset.
In order to provide the system with recommendations your IntentTimelineProvider
needs to implement the appropriate method:
struct MyTimelineProvider: IntentTimelineProvider {
func recommendations() -> [IntentRecommendation<ConfigurationIntent>] {
return [
IntentRecommendation(
intent: ConfigurationIntent(),
description: "My Intent Configuration"
)
]
}
}
The
ConfigurationIntent
type you see in the sample above is based on the default implementation you get when creating a watchOS widget extension via Xcode. In your code, the type is most probably named differently.
recommendations()
has a very straightforward implementation. All you need to return is an array of pre-configured INIntent
objects wrapped within an IntentRecommendation
. Additionally, you can give each recommendation a description
. This string will be displayed below the widget selection on watchOS.
Let us have a look at the structure with an example.
Example
Imagine we want to implement a widget which shows a colour based on a set of pre-defined colours. The first step is to add an intent configuration which looks like the following:
The timeline provider could look like the following:
struct BackgroundWidgetIntentTimelineProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: BackgroundColorConfigurationIntent())
}
func getSnapshot(for configuration: BackgroundColorConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: BackgroundColorConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let nextUpdate: Date = .now.addingTimeInterval(15 * 60 * 60) // Update every 15 minutes
let entry = SimpleEntry(date: .now, configuration: configuration)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
It implements the necessary methods to handle timeline entries, snapshots and placeholders. Before we move on, let's make our lives a little bit easier by implementing two extensions on the system-generated types from the intent definition file. One for easy iterating over the different enum cases and one to get a string representation of a colour.
extension BackgroundColor: CaseIterable {
public static var allCases: [BackgroundColor] = [
.red, .green, .blue, .pink, .purple
]
}
extension BackgroundColor: CustomStringConvertible {
public var description: String {
switch self {
case .red:
return "Red"
case .blue:
return "Blue"
case .green:
return "Green"
case .pink:
return "Pink"
case .purple:
return "Purple"
default:
return "Default"
}
}
}
Perfectly prepared, we can now implement the recommendations. Since there is no configuration UI for the user available on watchOS, we provide the system with an intent configuration recommendation for each of our defined colours.
func recommendations() -> [IntentRecommendation<BackgroundColorConfigurationIntent>] {
return BackgroundColor.allCases.map { color in
let intent = BackgroundColorConfigurationIntent()
intent.backgroundColor = color
let colorName: String = color.description
return IntentRecommendation(intent: intent, description: colorName)
}
}
That's it already. With this implementation in place, we have provided the system with enough information to let the user choose from a set of pre-configured complications.
If you want to reload your recommendations you can utilise WidgetCenter.shared.invalidateConfigurationRecommendations()
(Apple Documentation), which refreshes the configuration. This comes in handy for example when new data arrives and you want to update your recommendations.
The Bug
If you remember, I also mentioned a weird bug I encountered when implementing this. Within the initializer of a IntentRecommendation
we provide a description which is shown in the selection UI. As soon as this is not a simple string but an interpolated string, the widget extension seems to crash.
func recommendations() -> [IntentRecommendation<BackgroundColorConfigurationIntent>] {
return BackgroundColor.allCases.map { color in
let intent = BackgroundColorConfigurationIntent()
intent.backgroundColor = color
return IntentRecommendation(intent: intent, description: "Name: \(color.description)")
}
}
None of the previously working widgets is shown or even selectable any longer. It feels like the extension process is crashing immediately without any warning. Until now I did not find out why the process is crashing. Just by looking at the documentation providing an interpolated string as the description seems to be valid -> documentation, since StringProtocol
also conforms to ExpressibleByStringInterpolation
which should allow interpolation.
The workaround for this bug is to place the interpolated string in a dedicated property and use this instead of directly providing the string: The extension starts to work again.
func recommendations() -> [IntentRecommendation<BackgroundColorConfigurationIntent>] {
return BackgroundColor.allCases.map { color in
let intent = BackgroundColorConfigurationIntent()
intent.backgroundColor = color
let description: String = "Color Name: \(color.description)"
return IntentRecommendation(intent: intent, description: description)
}
}
If anyone knows why this bug appears, please reach out to me via Mastodon or Twitter. I would be highly interested in the root cause of this issue.
Conclusion
In this small article, I showed you how you can provide your users with pre-configured widgets on platforms where the system does not provide a native UI for intent definition files. Even further we also did have a look at a weird bug that can cause the widget extension to silently crash and showed you an, also not beautiful, workaround for that. I hope you learned something new today.
See you next time! 👋