Teabyte

Mobile app development for iOS and Swift

Widget Intent recommendations on watchOS and a bug

2023-02-22

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.

iOS Calendar Widget configuration

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.

iOS Calendar Widget configuration

In order to provide the system with recommendations your IntentTimelineProvider needs to implement the appropriate method:

MyTimelineProvider.swift
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:

Background Color Intent Configuration Background Color Intent Configuration Color enum

The timeline provider could look like the following:

BackgroundWidgetIntentTimelineProvider.swift
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.

BackgroundColor+Extensions.swift
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.

BackgroundWidgetIntentTimelineProvider.swift
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.

iOS Color Widget Intent Configuration

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.

BackgroundWidgetIntentTimelineProvider.swift
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.

BackgroundWidgetIntentTimelineProvider.swift
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! 👋