Add configuration and intelligence to your widgets
Widgets are at their best when they show up on someone's Home screen or in the Today View at the right time and provide actionable, relevant information. We'll show you how to build configurable widgets to let people create a personalized Home screen experience, and that take advantage of system intelligence to help people get what they want, when they want it. Learn how to customize your widget's configuration interface, and how to appear within Siri Suggestions or at the top of a widget Smart Stack based on user behavior or by letting the system know when there's new, timely information.
For more on widgets, watch "Meet WidgetKit". And for more on system intelligence, watch the collection "Design for Intelligence."
Welcome to "Add Configuration and Intelligence to Your Widgets." My name is Ian, and I'm here with my colleague Kelvin.
In this talk, we'll show you how to make widgets configurable, and show you how a configurable widget can help the system work in more intelligent ways.
New in iOS 14 and macOS Big Sur, we have a whole new widget experience. Widgets allow your app to surface new information to people in exciting new ways.
Widgets can live in more places than ever. They can live on pages of the Home Screen, as part of Stacks, on the Today View, and in macOS Notification Center.
Powering those widgets is WidgetKit. WidgetKit allows you to create multiple widgets, each to solve its own unique problem. Each widget can support different sizes and layouts. Let's take a look at an example.
We've built an example app that lets you view all of the transactions on your credit cards. It lets you view information about your spending and track balances on all of your accounts. The two most common things that I do in the app are check balances and look at recent transactions. It would be really great to see this information right at a glance on the Home Screen, so we built two widgets for our app.
The first one is called "Recent Purchases," which shows a list of purchases on one of your cards. The other one is called "Due Date," which shows your next payment's due date and how much you owe.
Because I often want to see information about one card at a time, we added configuration to pick which card to show. I can change which card is shown by tapping on "Edit Widget" and then selecting a card.
Because we made our widget configurable, now people can create multiple different instances of the Due Date widget and show a different card in each.
Now let's look at what we're going to cover today. First, we'll talk about the basics of how to add configuration to your widget. Next, we'll talk about the types of information that you can ask the user to enter, and show how to populate the interface with data from your app.
After that, we'll talk about how you can customize the title, description and colors of the backside of your widget. And finally, we'll cover how adding configuration to your widget allows the system to better predict times to display your widget in a stack.
Let's start with the basics. The first piece of the system is the widget extension. Widget extensions contain SwiftUI code that renders the visual appearance of the widget. They also contain some metadata about the widget itself.
Before you get started, make sure that you have set up a widget extension already using WidgetKit and SwiftUI. If you haven't done that yet, make sure to go watch the "Meet WidgetKit" talk.
When your widget is configurable, you can specify which options to ask the user on the backside of the widget, and the system will display them for you. These questions are called parameters.
Here's our Recent Purchases widget. It has a "Card" parameter and a "Category" parameter. The Card parameter allows people to choose a card to display, and the Category parameter lets people filter the list to only include transactions for a specific category, like groceries, if they'd like.
To define which parameters are shown, we use intents, which is the same system that you can use to add support for Siri and Shortcuts to your app.
An intent contains an ordered list of parameters. Each parameter that you put on the intent will show up as a row in the widget's configuration UI.
You declare your intents in Xcode using an Intent Definition File. This file contains all of your intents, their parameters, and indicates which parts of the system they support. By compiling this file into your app, the system will be able to read information about your intents.
Once you define an intent, Xcode will also generate an intent class for you, containing a property for each parameter. For this widget, we have a ViewRecentPurchasesIntent class, with Card and Category properties.
An instance of this intent class will be passed into the widget extension at runtime, allowing your widget to know what the user has configured and what to display. Next, let's talk about the types of parameters that you can add to your intent. As I mentioned before, we build the widget configuration UI for you, based on the intent's definition. We support a wide variety of input types, which I'll walk you through now. When you specify "String" as the type of your parameter, the configuration UI shows a text field, and it will show a switch for the "Boolean" parameter. The configuration UI also supports numbers with a few different input mechanisms. For integers, it supports number fields and steppers, and for decimals, it supports number fields and sliders.
The configuration UI also has support for contact and location pickers. The "Person" type will show as a contact picker, and the "Location" type will show as a location picker.
The configuration UI also supports enumerations. You can make these enumerations static, or dynamically populate them with content from your app. We'll talk about dynamic options in more detail later. The configuration UI supports a number of other types as well, each with their own UI. Parameters can also support multiple values. For example, if your widget shows a list of events from your calendars, you can configure the intent to support selecting multiple calendars. New in iOS 14, Intents now supports fixed-size arrays. This is useful if your widget supports showing a fixed number of items, and lets you prevent the user from adding more than a defined number of items into the array. You can specify the number of items based on the widget size in the intent editor. Now I'm going to walk you through how to add rich configuration to your widget. The first step is identifying what you want to make configurable. Let's use the Recent Purchases widget here as an example. In the Recent Purchases widget, each widget can only show one card, but people may have multiple cards in the app, so it would make sense to make that configurable.
This allows people to have multiple Recent Purchases widgets on their Home Screen, each showing different cards.
It also would be useful to be able to filter by category. That way, people could have one widget to show transportation expenses and another widget to show food expenses.
So, in addition to making the card configurable, we are also going to allow people to configure the category.
Now I'm going to walk you through how we can make the Recent Purchases widget configurable. This is our project, where we already have our widget set up. You can see that the widget currently has a StaticConfiguration.
In order to make the widget configurable, we need to switch from a StaticConfiguration to an IntentConfiguration. The first step in switching to an IntentConfiguration is defining an intent that you'll use to configure the widget. You can define a custom intent inside of an Intent Definition File.
I already have one here in this project. You'll note that it is checked in to both the app and the widget extension targets.
Now let's create an intent using the plus button in the bottom left.
I'll be calling it ViewRecentPurchases.
Next we're going to check the "Intent is eligible for widgets" option.
This will allow the intent to work as a configuration for a widget. If you want this intent to also support the Shortcuts app, check the "Intent is user-configurable in the Shortcuts app and Add to Siri" option. We'll uncheck this option for this demo and just focus on widgets. We're also going to leave the "Intent is eligible for Siri Suggestions" checkbox unchecked for now, and my colleague Kelvin will walk you through how to add intelligence to your widget later in this talk.
We'll also set the category of this intent to "View" because the purpose is to view your transactions.
Now, using the plus button in the parameters list, I am going to add a parameter for the card.
The first step here is naming the parameter. This label will be displayed in the widget configuration UI. I'm going to name this "Card." Next, I need to pick a type.
Because a credit card represents an object inside of my app, I'm going to create a custom type to represent it. You'll see that I already created a "Card" type, which has a name and identifier, so I'll use that here for my type.
The list of cards will need to be provided by the app, and will change depending on who is signed in, so I will check the "Options are provided dynamically" checkbox.
We'll go into detail later on how to populate this dynamic list.
Since this intent won't support running in Siri, we can uncheck the "Siri can ask for value when run" option Next, because we want to allow people to choose the category, we'll add a "Category" parameter to the intent.
We can use an enum for the type of this parameter because there are a fixed set of categories. I already have an enum defined, so I will choose that.
We'll set the default value for the category to "All" so that people will see all of the transactions by default.
And we'll also uncheck the "Siri can ask for value when run" option for the category.
Now let's talk about dynamic options and search. In many cases, the data that you want to display in the widget configuration can vary by person, and can't be specified up front in the Intent Definition File. For example, the list of cards in our example app varies by person. You can accomplish this by implementing dynamic options.
You can enable dynamic options for any parameter on an intent by checking the "Dynamic Options" checkbox in Xcode. Enabling dynamic options will indicate to the system that it should consult your app to retrieve the possible values that a person can choose from, instead of allowing them to enter any value that they want. Once you enable dynamic options, two things will happen. First, the visual appearance of your parameter will change on the backside of the widget into a button that opens a modal list of options.
Second, two methods will be generated for you to implement in your app, one to provide a list of possible options, and a default value.
These methods are part of the intent handler protocol, which Xcode generates for you. You will need to make a class that conforms to this protocol, either in your app or in an Intents extension. This class will be asked by the system to provide possible options while the person is configuring your widget.
In the example app, the two methods that we need to implement are provideCardOptionsCollection and defaultCard. For provideCardOptionsCollection, we will load the list of cards that the user has set up and then call the completion handler. For defaultCard, we return the primary card that the user has set up.
The provideCardOptionsCollection method lets you either provide a flat list of cards or a list of sections containing cards.
For example, we can display a section for each type of card, like credit or debit. If you use sections, your UI will look like this.
Now all of my credit cards show under the "Credit" section, and I can see more sections containing other types of cards.
By default, the search bar at the top will filter the options that you provide. In some cases, your app might have more data than you can easily provide up-front in a list. If that's the case, you can provide search results as the person types. These results can include options outside of the standard options that you provide dynamically.
To do this, make sure to check the "Intent handler provides search results as the user types" checkbox and also provide a prompt.
If you check the checkbox, the provideCardOptionsCollection method will get a search term parameter.
When a person first looks at the list, this method will get called with a nil search term, and then as they start typing, the method will get called again with the updated search term. Now let's do a quick demo of how you can implement dynamic options. Continuing from our last demo, let's go ahead and implement returning the list of cards to the user dynamically. The first step is setting up the intent handler for the ViewRecentPurchases intent. We're going to put our intent handler inside of an Intents extension. I already have an Intents extension set up for our other widget, so I can add my code there.
The next step is making our intent handler conform to the ViewRecentPurchases intent handling protocol.
You can see that this protocol has a single required method, which is the provideCardOptionsCollection method. We need to implement this such that it returns a list of cards inside of the app.
First, I create custom Card objects from the objects inside of my app. Then I put them into the INObjectCollection, which allows me to add sections if I want.
I don't have any sections in mind here, so I will just create the collection from a list of items, and then call the completion handler with the created collection.
There's also an optional method to provide a default card.
The default is used when a widget is dragged onto the Home Screen for the first time, so it's really important to provide a good default.
Now that we've created our intent and implemented our intent handler, we can adopt the IntentConfiguration in our widget.
It's very similar to the StaticConfiguration that we had before, but it takes an intent type as an argument.
You'll also need to switch your TimelineProvider to be an IntentTimelineProvider.
It's very similar to the default TimelineProvider, but the methods have an additional intent argument that you use to determine what to show.
Here, we’ll need to take the card from the intent and use that to determine which purchases to show. Previously, it was just showing the purchases from the default card.
Let's run the code and try to configure the widget.
Notice that when I drag out the widget, it shows me transactions from the default card.
This is because I implemented the default card method.
Now if I go and flip it around and tap on the "Card" button, it shows me a list of cards.
Tapping on the "Category" option also shows a list of categories.
Now let's talk about a few ways that you can further customize the appearance of your widget configuration UI. You can customize the title and the description by using the SwiftUI modifiers called "configurationDisplayName" and "description" in your widget extension.
Next is colors. You can style the background and the accent color of the widget configuration UI to match your app's color scheme.
To do this, you'll need to add named colors to your widget extension's asset catalog. Here we're adding one for the accent color and another one for the background color.
And then you'll need to add the names of your colors to your widget extension's build settings for "Global Accent Color Name" and "Widget Background Color Name." Another thing you might want to do is hide and show specific parameters based on another parameter. For example, in the Calendar widget, if you turn off the "Mirror Calendar App" switch, the "Calendar" parameter appears, and you can manually choose which calendar to show.
Let's take a look at the Intent Definition File to see how to specify the relationship between Mirror Calendar App and Calendar.
To specify that we only want the "Calendar" parameter to show up when the "Mirror Calendar App" option is off, make the "Mirror Calendar App" parameter a parent of the "Calendar" parameter.
To do that, we'll select the "Calendar" parameter, then select "Mirror Calendar App" as its parent.
Then we'll set it up to show only if the parent has the exact value of "false". Now the "Calendar" parameter will only show up if the "Mirror Calendar App" is turned off. That's it from me. Now over to Kelvin to talk about system intelligence and widgets.
Hi, I'm Kelvin, and I work on the Proactive team here at Apple. Next, we're going to talk about system intelligence. Widgets are one of the most exciting new opportunities across iOS, iPadOS and macOS. On iOS 14, not only can you add widgets to the Home Screen, you can stack multiple widgets together and provide access to a variety of widgets in one place by easily swiping through.
Everyone will find a different way to take advantage of this, but it's easy to imagine that you could group multiple widgets together that would be useful as you progress through your day. Now, what's exciting about Stacks is that the system can automatically rotate widgets to the top of the stack to provide useful and timely information.
Let me walk you through the underlying design principles that drive stack intelligence, and how you should implement some new APIs to ensure your app is part of this new Home Screen experience. Let's dig a little deeper into what makes for a good Smart Stack.
Stacks should provide timely and glanceable information with obvious value to the user.
You want your widget to be surfaced when you have timely information relevant to the people using your app, like when you know there is a thunderstorm coming, but not when you're just updating the temperature regularly. Broadly speaking, the system will try to surface widgets based on two reasons. The first is user behavior-based. We want to surface widgets that provide information the user typically looks for at a particular time. For example, if someone frequently launches your weather app to check on the weather, the system can instead surface your widget with that information to provide quicker access to what they're looking for.
The second is based on relevant information from your app. So, for example, if a thunderstorm is happening, your widget can inform the system that it has a highly relevant update, and the system will consider surfacing your widget to the top of the stack.
Now let's talk about the APIs you can use to provide the information the system needs to surface your widget.
Let's start with user behavior.
So the system wants to surface your widget when someone is typically looking in your app for information, and in iOS 12, we introduced the concept of Shortcuts and custom intent donations, which provides a way for the system to understand what people do in your app. And using this information, the system provides predictions in Spotlight on what actions they want to perform.
Now, new in iOS 14, these same donations will also inform the system about when would be a good time to surface your widget.
So now, let's continue from our previous example with the Recent Purchases widget. Here we have already set up an intent for widget configuration. It allows a user to specify the credit card and the category they want to view. Now, to enable system intelligence to rotate to our widget, we have to set this same intent for donation by our host app.
The idea here is we want to inform the system when a user is checking a particular credit card in our app, and we will do so by donating this intent. We'll describe what happens under the hood in just a bit. So for now, let's dive into some details about how to set it up.
First, we need to mark this intent as eligible for Siri Suggestions, and that opens up the bottom section here labeled "Suggestions." And for our example, we want the system to predict when someone will check a particular credit card, and we want it to match any widget with that same card configured. In order to convey that, we need to add a "Supported Combinations" with just the Card parameter.
And so now that we have set up our custom ViewRecentPurchases intent, we need to have our host app donate it whenever the user views recent purchases in our host app. And to do that, we create an INInteraction object and call the donate function, passing in our intent.
And since we've specified a "Supported Combinations" as just the Card parameter, even if we provided the Category parameter in our donation, the system will only consider the Card parameter. And that's all you have to do. The system will do the rest. Now let's take a closer look at what the system does with your donations under the hood. So let's say someone views grocery purchases for our AcmeCard regularly at noon, and checks their SoupPay card for restaurant purchases in the evenings. Now, our host app would donate intents like this.
And based on these donations, the system would recognize that the user typically checks their AcmeCard at noon. And then on Friday at noon, the system would predict the intent for viewing AcmeCard and would surface our widget, as long as they have configured it to show their AcmeCard regardless of the category.
But what if instead we added both Card and Category as a supported combination? Let's look at what would happen. Now, since Category is part of the supported combination, you are telling the system that it is an important parameter. Then on Friday at noon, the system would predict the intent for viewing AcmeCard in the "Groceries" category, which would only surface widgets where the user has explicitly configured both parameters to show the AcmeCard for the "Groceries" category.
So, in short, "Supported Combinations" is the way to communicate to the system which configurable parameter truly represents the information people are looking for.
We walked through a lot of steps there, so to quickly summarize, first, set up a configuration for your widget with an intent. Second, mark it as eligible for Siri Suggestions to allow the system to predict it.
Next, configure "Supported Combinations" for only the parameters that you want the system to predict. And finally, donate the intent when the user views that information in your host app. Now let's talk about the second scenario, where you want the system to surface your widget when you have important, relevant information.
So, a quick recap. Using WidgetKit, you provide timeline entries that will determine what your widget will look like at various points in time. It works great whether your widget can schedule your entries ahead of time, like this weather example where you can provide forecasts for future views. It also works if your widget needs to react to new information in real time, like our Recent Purchases example, where you can only provide each entry as they occur. So now, let's say we know that people using our app want to be alerted if they got a charge for over $50.
How can you inform the system that your widget has relevant information for the Acme Books purchase? You can convey that information by supplying a TimelineEntryRelevance object with your TimelineEntry.
A TimelineEntry has effectively three components: a date time stamp that determines when this entry should be rendered, the view that should be rendered, and the relevance of this entry. The relevance is a TimelineEntryRelevance object, which has two fields: a score and a duration.
And here's what it looks like in Swift. Let's start with the score. The score is a value indicating how relevant an entry is compared to all entries provided in the past. The range and scale is largely up to you to define, as the system only considers the score in relation to other entries. The only exception being a score of zero or lower will indicate to the system that your widget currently has no relevant information and should not be surfaced. Now, coming back to our example, we want to convey to the system that we have important information when there is a purchase of more than $50. We can simply provide a score of one when that happens to emphasize the importance, and a score of 0.1 for all other purchases. That way, there will be a slight chance the system might surface our widget for the minor purchases, but will try to prioritize for the big purchase. When there is no important information, like when we have no recent purchases at 8:04 a.m., we set a score of zero. And keep in mind, it doesn't matter what scores other widgets provide. Scores are only compared within the scores you provide. Now, on the other hand, let's say we think the amount spent is a good indicator of relevance. We can set the amount spent to be the score, and the system will likewise prioritize surfacing the expensive purchases. Next, let's talk about duration. The duration is a field for situations where you have a well-defined period of time where the relevance score will be fixed. Otherwise, feel free to leave duration as zero, which will mean the relevance score will last until the next TimelineEntryRelevance is received.
Here's an example to highlight how duration can be used. We want to build a basketball widget that shows the progress of a game as it occurs. So, at the beginning of the game, we can create an entry with a score of one and a fixed duration for the length of the game. During the game, we can continue to make TimelineEntry updates without affecting the relevance by just leaving the TimelineEntryRelevance field as nil. Setting the entry as nil is how you can tell the system to ignore this update for relevance purposes. So to wrap up, with Smart Stacks, we have an opportunity to surface your widget on top of a stack. You can make this work for your widgets in two ways. First, by donating intents from your app that match configuration of your widget, and secondly, by providing TimelineEntryRelevance when your widget has important information. Thanks for watching. We can't wait to see what kind of configurable, intelligent widgets you will build.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.