During WWDC 2023 Apple announced that the AppIntents
framework can now also be used to create Widget configurations. Since last year's announcement of AppIntents
I was looking forward to also configuring Widgets with AppIntents. Now it's here and I want to share how to create an AppIntent
based widget. I will assume some basic knowledge of how widgets work in general throughout the article. I can highly recommend checking out the official documentation about Widgets in that case.
Create an AppIntentConfiguration
Previously we used StaticConfiguration
or IntentConfiguration
to configure widgets. Starting from iOS 17 it's now possible to use AppIntentConfiguration
to use AppIntents
for configuration.
struct FavouriteIceCream: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: "com.test.icecream-favourite",
intent: SelectFavouriteIceCream.self,
provider: IceCreamProvider(),
) { entry in
IceCreamView(entry: entry)
}
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
The additions to previous configuration options are that intent
can now be of any type conforming to AppIntent && WidgetConfigurationIntent
and provider
needs to conform to AppIntentTimelineProvider
.
Create AppIntent
An AppIntent
type is essentially a way to make app content and functionality available to the system. With the addition of WidgetConfigurationIntent
the type can now also be used to create configurations for widgets. Let's create a bare minimum app intent for our widget.
@available(iOS 17.0, macOS 14.0, watchOS 10.0, *)
struct SelectFavouriteIceCream: AppIntent, WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select favourite ice cream"
static var description = IntentDescription("Selects the user favourite ice cream")
@Parameter(title: "IceCream", optionsProvider: IceCreamOptionsProvider())
var iceCream: String
@Parameter(title: "Site", optionsProvider: ToppingOptionsProvider())
var topping: String
}
The configuration lets users select an ice cream as well as a topping. It uses the Parameter
property wrapper to make the properties modifiable in the widget configuration UI.
For both options, we can declare a DynamicOptionsProvider
which is responsible for giving the user options to choose from. For the sake of simplicity, we only allow String
types here. It is also possible to declare custom types and custom enums as parameters. For this, I recommend reading the official documentation about custom data types.
extension SelectFavouriteIceCream {
struct IceCreamOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
["Vanilla", "Strawberry", "Lemon"]
}
}
struct ToppingOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
["Chocolate Sirup", "Sprinkles", "Peanut Butter", "Toasted Coconut Flakes"]
}
}
}
Create AppIntentTimelineProvider
Last but not least we need to create the IceCreamProvider
conforming to the new AppIntentTimelineProvider
. These new types of protocol requirements are very similar to IntentTimelineProvider
. The only differences I found are that an AppIntent
is used for configuration and the methods for snapshot
and timeline
are using async/await instead of completions. As with the previous code, the provider is rather straightforward for our example:
struct IceCreamProvider: AppIntentTimelineProvider {
typealias Entry = IceCreamEntry
typealias Intent = SelectFavouriteIceCream
func placeholder(in _: Context) -> IceCreamEntry {
.sampleData
}
func snapshot(for configuration: SelectFavouriteIceCream, in context: Context) async -> IceCreamEntry {
return .init(iceCream: configuration.iceCream, topping: configuration.topping)
}
func timeline(for configuration: SelectFavouriteIceCream, in context: Context) async -> Timeline<IceCreamEntry> {
let nextUpdate: Date = .now.addingTimeInterval(60 * 15) // 15 minutes
let entry = IceCreamEntry(iceCream: configuration.iceCream, topping: configuration.topping)
return Timeline(entries: [entry], policy: .after(nextUpdate))
}
}
With the provider being in place we have successfully created an AppIntent
based widget. Great! In the next two chapters, I want to dive a little bit into more advanced techniques for widgets with app intents.
Create dependencies between @Parameters
In the past, it was possible to create dependencies between parameters of your intent definition files used to configure widgets. With AppIntents
this is possible as well. For this we are going to use IntentParameterDependency
. It's a property wrapper we can use within our DynamicOptionsProvider
to create a dependency to a parameter of an app intent declaration.
In the context of the ice cream example let's say different toppings are only available for certain ice cream types. We declare a dependency to the SelectFavouriteIceCream
intents .iceCream
property. The type of the intent
property will be treated as Optional<SelectFavouriteIceCream>
from which we then can read the respective declared dependencies. In this case the .iceCream
extension SelectFavouriteIceCream {
struct ToppingOptionsProvider: DynamicOptionsProvider {
@IntentParameterDependency<SelectFavouriteIceCream>(
\.$iceCream
)
var intent
func results() async throws -> [String] {
switch intent?.iceCream {
case "Vanilla":
return ["Chocolate Sirup"]
case "Strawberry":
return ["Sprinkles"]
case "Lemon":
return ["Toasted Coconut Flakes"]
default:
return ["Chocolate Sirup", "Sprinkles", "Peanut Butter", "Toasted Coconut Flakes"]
}
}
}
}
The IntentParameterDependency
can also be used when querying for custom types as described in official documentation about custom data types.
Share AppIntents across Frameworks
Another valuable addition brought to AppIntents
framework is the AppIntentsPackage
protocol.
With this protocol, you can instruct the compiler to find app intents that are used in frameworks. Previously, app intents needed to be declared either in-app or extension targets directly to be correctly recognized. Now we can also move them to specific frameworks and have them correctly picked up.
This feature is especially helpful for modularized applications where intents might be declared in different features which themselves are frameworks.
To make app intents available from frameworks two things are necessary:
- Frameworks need to declare a type conforming to
AppIntentsPackage
- In the top-level extension or app target you need to pick up the types and combine them.
It is like bubbling up the information of app intents in your dependency tree. Let us examine this in the following setup. The WidgetExtension
depends on IceCreamUI
which itself depends on IceCreamData
. The latter one contains the app intent definitions.
Target: WidgetExtension
-> Target: IceCreamUI
-> Target: IceCreamIntents
In order to make the app intents available in the WidgetExtensions
the following chain needs to be implemented:
// Target: IceCreamIntents
public struct IceCreamAppIntents: AppIntentsPackage { }
// Empty delcaration tells compiler to search for all intents in this target
// Target: IceCreamUI
import IceCreamIntents
public struct IceCreamUIAppIntents: AppIntentsPackage {
static var includedPackages: [AppIntentsPackage.Type] = [
IceCreamAppIntents.self // Make child intents available here
]
}
// Target: WidgetExtension
import IceCreamUI
@main
struct WidgetsBundle: WidgetBundle {
@WidgetBundleBuilder var body: some Widget {
...
}
}
@available(iOSApplicationExtension 17.0, *)
extension WidgetsBundle: AppIntentsPackage {
static var includedPackages: [AppIntentsPackage.Type] = [
IceCreamUIAppIntents.self // Make child intents available here
]
}
With this chain, we could potentially build a tree from the bottom up to collect all app intents from different frameworks until we reach the root at the app or extension target.
In my opinion, this is a very helpful addition to the AppIntents
framework. Especially if you want to share app intent definitions across multiple frameworks.
Conclusion
Thanks a lot for reading this far! This year's additions to AppIntents
to let us allow them also to configure widgets brings us one more step closer to code-only projects. It seems to be clear that Apple favours code as the source of truth for the upcoming years. Maybe to even ditch the Xcode project files 🤔? Let's see.
I hope you found this article interesting and can use some of the information provided in your daily development work.
If you have any questions or found some issues, please don't hesitate to reach out!
See you next time 👋