WidgetKit: Advanced development - Part 2
By: Marco Guerrieri
Tags: iOS, Ios 14, Swift, Engineering, Widgetkit

Introduction

This is the continuation of the WidgetKit: Advanced development - Part 1 article where we had a look at various advanced development techniques for the new iOS Widgets, here we will focus on Static and Dynamic Configuration of the widget and implement Widget User Interactions

Widget Configuration

We have created nice widgets but, at the moment, we aren’t providing the user with a way to configure them, so let’s see how we can implement the configuration of the widget.

Sticking to the Event app of the part 1 examples, we can imagine that our users might be interested in a certain type of events, so wouldn’t it be great to show just the events of that type? Maybe the users loves Movies or Travels, but they’re not really into Art or Music. In the previous article we were fetching all the events, now it’s time to allow the user to filter the events displayed in the widget in an easy way, just by holding a finger on the widget and selecting “Edit Widget”.

WidgetKit allow us at do that in 2 ways, the first is a simple Static Configuration, the second one is a more complex Dynamic Configuration, let’s have a look at them.

Static Intent Configuration

The static intent configuration is quite easy and simple. First, for static configuration I mean “hard-coded” configuration where we provide a configurable parameter and the associated values that can take.

Any widget can be configured through parameters of various types, and each widget will save its own configuration state for itself. This means a user can have 4 small size widgets of the same app on their home screen, with each one configured differently.

The easiest way to have a configurable widget is to be sure that Include Configuration Intent is checked when you add the Widget Extension to your project. When you do so, you will notice some differences in respect to a Widget Extension that does not include it:

Provider class of the widget is no longer a TimelineProvider but an IntentTimelineProvider , and this also affects the getSnapshot and getTimeline functions, where we see one more parameter added at the beginning of the input declaration, the configuration parameter.

class of the widget is no longer a but an , and this also affects the and functions, where we see one more parameter added at the beginning of the input declaration, the parameter. TimelineEntry class now has a shiny new configuration property

class now has a shiny new property A new file has been created in your widget folder with the .intentdefinition file extension

The new file is the important one, when you select it, the Intent editor is opened.

NOTE: Intents have been available since iOS 10.0, but have mostly been used for interactions with Siri. It’s a quite a big topic, but you can easily find various articles about that. For our purpose there is no need to know too much about this topic, so don’t be afraid if you have never used Intents.

In the Intent Editor you will see a lot of parameters and checkboxes, but the only one we want to be sure is checked for now is Intent is eligible for widgets:

Now, let’s add a configuration, hit the + button below the list of Parameters. Adding a parameter and selecting it in the list will show you a lot of settings applied to that parameter. Each parameter can have its own type: String, Booleans, Enums or even new types (we will see this one in the dynamic configuration paragraph).

As mentioned, we want to let the user filter the events shown by category, so, first, we add a parameter called category:

Then open the Type dropdown and select Add Enum.

Below the Custom Intents part, in the left menu, you should now see Enums and your newly created one - we’ll call it EventCategory, and, for display name, something simple like “Event Category”. Once we’re finished, we can add the cases of this enum like music, movie, art, travels and all:

You have just effectively created a static configuration for your widget! If you run the simulator, hold tap your widget, hit “Edit Widget” and you should be able to see the configuration view:

NOTE: As you can see here the “All” case is already selected for events category, you can easily set the default value in the Intent Editor

It’s nice to see that but it’s still somewhat useless at the moment, because changing the category will not affect what we show in the widget at all, we need to add some code to let the widget understand what to do with our configuration.

Going back to our widget Provider, inside our beloved getTimeline function, we have, as said before, the configuration parameter in the declaration of the function, we just have to use that parameter to change the way we create the Timeline:

Here we are, the getTimeline now uses the configuration to set the correct filter to the EventsLoader and make a call that returns the event of just that category!

NOTE: if Xcode complains that configuration.category does not exist, just run the build of the widget target again until it recognizes the new parameter

Running the app, add three widgets of the same family, configure each widget with a different category and we will obtain three widgets with three different outcomes:

Dynamic Intent Configuration

The Static configuration has been useful, but what happens if in the future the categories of the events changes? Maybe in the backend History and Nature categories are added, and if that happens, we will have to make changes to our app code and resubmit the app, while users with the old version of our app will not have those categories until they will update it. Definitely not ideal. At this point the dynamic configuration comes to help us.

The dynamic configuration allows us to provide the user configurable parameters that can assume various values that are fetched directly from the APIs if needed.

Let’s go back to our Intent Editor, remove the EventCategory enum from the Enum list, and for our category parameter Type choose Add Type. You are now able to name your new type, we can again call it EventCategory.

As you can see, you can again choose the Display Name that will be shown in the Edit Widget view. Apart from that, now we have also 2 properties called identifier and displayString, which are pre-created and cannot be modified, for our example just add a new parameter called value of type String.

Let’s build the widget, making sure Xcode does not complain about anything, (if it does so about a “Parameter voice-only prompt…” error, just go back to the category parameter in our parameters list of the Configuration and deselect “Siri can ask for value when run”).

Be sure to check “Options are provided dynamically” in our parameter configuration (this is needed to actually make it possible for us to provide the values of the parameter).

You should now be in a similar situation:

Everything should be fine, now we have to add a way to use that parameter when creating the timeline. This is not straight forward as before because we are using a class now, not an enum with defined values, and if you run your widget, in the widget configuration view you will not be able to see any value to select.

First we need to provide to the widget the values that our EventCategory can assume. To do that we need to add an Intent Handler that will be in charge to fetch and provide those values to the widget.

Select File -> New -> Target and then select Intents Extension. In the next screen just provide a name, like “EventWidgetIntentHandler”, choose None as Starting point and be sure that Include UI Extension is unchecked. Continue and when prompted, choose to activate it.

Now we have a new folder in our project, containing just Info.plist and the IntentHandler.swift.

First we need to let this target understand what type of intent can be handled. Go to your project settings, choose the EventWidgetIntentHandler target and add a new Supported Intents entry ConfigurationIntent:

You have also to select your widget .intentdefinition file and add it to the EventWidgetIntentHandler target:

Now open the IntentHandler.swift file inside EventWidgetIntentHandler folder and add to the IntentHandler class the protocol ConfigurationIntentHandling, required to correctly handle our ConfigurationIntent:

class IntentHandler: INExtension, ConfigurationIntentHandling {

}

If you build now, you should have an error in the IntentHandler class because it’s not conforming to the protocol ConfigurationIntentHandling, so you need to add the provideCategoryOptionsCollection method that will be in charge to actually fetch the values for our configuration parameter. So we just have to fetch the categories, transform each category into an EventCategory type, create an INObjectCollection and then call the completion block:

NOTE: the value property that we added to the EventCategory type is quite useless, but was valuable to provide a better example of how to implement a dynamic configuration with custom parameters instead of only the identifier and name.

Now run the application and you should be able to configure the widget with the categories obtained from the backend as expected!

Widget User Interactions

Apple also wanted to be sure that widgets wouldn’t be used too much as apps, so the interactions that you can have with the widgets are reduced to only tap gestures.

By default, when the user taps a widget, the system will just open the app, so the app will appear from its starting point or will return to the foreground if it was in the background. Sometimes this is enough, but it’s also possible to improve this user experience.

Widgets, as mentioned, can have different sizes, and the size of a widget also impacts the interactions it supports. In fact if the widget is a small one, the interaction is reduced to a single tap that can be done on any part of the widget. Instead, in the medium and large sizes, various parts of the widgets can be set to have a different behaviour when tapped.

Let’s get back to our events list example, in the small widget I am showing just the first event that will occur, while in the large one I am showing the next three events:

This is an ideal design to understand the interactions allowed:

if the user taps any part of the small widget, the app will be opened and the details of the event (the only one shown in the widget) displayed

widget, the app will be opened and the details of the event (the only one shown in the widget) displayed if the user taps on one of the events inside the large widget, the app will be opened and the details page displayed will be the one of the event that the user tapped.

For example in my events app it will open this detail screen:

To implement this we will use the new method of the swiftUI View .widgetURL(URL) and the swiftUI control Link to open the app passing a parameter to show the tapped event details.

Using the .widgetURL(URL)

The .widgetURL(URL) is the method that defines the URL called when the widget is tapped (no matter where). You have to implement it just once in the widget view, and it can be used on all widgets size.

Adding the .widgetURL(URL) is quite simple, if you have a view containing the widget design just call it at the end:

If you don’t have a container view, just add and use it on a ZStack like this:

NOTE: In these examples I am not really checking if the events[0] exist, be careful about that, also the URL passed is nullable, so if you don’t pass a URL the widget will just open the app as for the default behaviour.

Using the Link

The Link is the SwiftUI control that you can instead use to define different tappable areas of the widgets and, as mentioned, works only with the medium and large size. Let’s say we have an array of events, each event has its own EventView (I mean a tile/cell with that), and should also signal the app which event detail should display when tapped:

NOTE 1: In this example my view is used for large widget, I am checking it in the main View of the widget.

Now that we have implemented the correct links to our app, we should also handle the URL passed, right? But where? According to Apple documentation, it depends on the life cycle of your app. Basically the system activates the app and passes the URL to onOpenURL(perform:) , to (_:open:options:) , or application(_:open:) .This point wasn’t really clear to me, so my next step will be to learn more about Deep Linking in iOS, but I can show you what I have done so far.

In my example app, I am receiving the URL in the SceneDelegate. When the app is closed, the received URL is handled in:

While when the app is already opened, the received URL is handled in:

It works fine, and I am happy with the outcome, but I guess this can be improved a lot.

It’s also worth bearing in mind, as Apple says, this really depends on your existing app life cycle and architecture, as you can see in the Respond to User Interactions paragraph here.

Conclusion

Widgets are really cool, flexible, and Apple made a really nice and easy-to-use WidgetKit. Things seem already at a good point and hopefully we will soon have widgets on our devices. That said I have encountered strange bugs (flashing widgets, non-loading widgets, crashes) that were fixed mostly by just restarting the simulator.

Let’s see what the outcome will be when everything comes out of beta.