Hacking iOS14 Widgets
By: Riccardo Cipolleschi
Tags: Swift, Swiftui, Apple, Apps, Mobile App Development

DEVELOPMENT

Hacking iOS14 Widgets

With iOS14, Apple introduced a new framework to implement widgets: WidgetKit . Widgets are small pieces of UI, rendered in the Springboard, that offer quick access to some information from our apps. They come in 3 sizes: small, medium, and large. A small widget occupies the space of 4 app icons. A medium one, the space of 8 app icons. A large one, the space of 16 app icons.

On the left a large and a medium widget. On the right a small widget.

Widgets can only be implemented in SwiftUI and, by quoting the documentation:

Widgets present read-only information and don’t support interactive elements such as scrolling elements or switches. WidgetKit omits interactive elements when rendering a widget’s content.

Let me stress it: Widgets present read-only information. I find this limitation annoying. There is a whole category of apps that can benefit hugely from a little interaction: tracking apps, for example. It could track drinking water, fasting, breathing, eating, mood… Anything you can think about. It could be extremely annoying to fully open the app, wait for it to connect to some backend, render the UI, maybe present a paywall, and, once it is fully loaded, just tap a button and close it.

Therefore, I explored deeper the widgets, searching for a workaround for this limitation, and today I want to share with you how we can overcome it.

Ingredients

To have an interactive widget, we leverage a couple of concepts of iOS:

URL types

App groups

URL Types

Every iOS app can define a custom URL Type to which the app can respond. We can define a custom URL protocol and, whenever another app tries to open an URL with that protocol, iOS will try to open the app associated with the URL.

Fun fact: this works also from Safari! See the video below

To set up a URL Type :

Select your project in the Navigator . Select the project in the Target section. Select the Info tab. Expand the last dropdown panel called URL Types . Fill the form.

To fill the form, just add the identifier of the app and the URL protocol in the URL Schemes field. In the example, whenever an app tries to open an URL that starts with the drinkit:// protocol, the system will try to open our app. Notice that the scheme is added without the :// .

Why are URL Types interesting? In WidgetKit there are only two types of controls we can leverage. Both these controls let us open a link. The link can start with the protocol we just defined and as soon as our app is launched, we can perform some operation with it.

App Groups

App groups are a capability we can add to one or more apps and to any extension we want. When we have one or more apps that belong to the same app group, they can share some resources. For example, they can share an area of the keychain or a specific suite for the UserDefaults .

We can use an app group to let the App and the Widget communicate. Any other mechanism can work: you can rely also on some network communication (in this case you won’t need the app group).

To set up the App Group :

Select your project in the Navigator . Select the project in the Target section. Select the Signing & Capabilities tab. Tap on the small + in the top left corner. Select App Groups and configure it.

This process may take a while and you need a developer account in order to create the app group: Xcode tries to generate a provisioning profile that supports that capability.

Note: Aall the app and extensions that need to belong to the app group require to add such capability. In our example, we have to do it for both the app and the widget. However, we need to create a new App Group only the first time: in the cases we just have to select the same group.

The App

Now that we have all the ingredients, let’s create the app and the widget. To keep the example simple but real, I prepared a water tracking app. Both the UI and the logic are extremely simple: a short video should be enough to understand how it works.

Implementing the whole app is out of the scope of this article. There are only two relevant parts of the App that we should investigate:

the Storage , shared between the app and the widget;

, shared between the app and the widget; the AppDelegate.application(_, open url:, options:) -> Bool function, to implement the logic when the app is opened from a URL.

The Storage

The storage is simply a singleton that can be accessed by every part of the application, including the Widget. To allow both the widget and the app to access the same logic, we need to set properly the Target Memebership of the Storage file.

I chose this approach for this example because it is quick to implement and easy to understand. However, I strongly suggest you DO NOT USE this approach in a production environment. Code can be shared in a more elegant and maintainable way by using frameworks. Furthermore, a Singleton is rarely a good solution, given that it is hard to test and to inject. There are several other mechanisms to handle the storage and the dependencies: have a look at Katana or the Composable Architecture if you are interested in the architecture topic.

Here the code of the Storage :

The beginning of the file contains some private static variables that are used to access the right suite for the UserDefaults and the right value within the shared UserDefaults .

We also have a semaphore to avoid concurrent access to the storage. We use it within the update function. If you are curious about how to protect your resources, I wrote this article which explains this pattern.

The interesting bit is line 42 : with this API we inform the widget that the data has changed and it has to be updated.

Open URL

The second interesting part of the app code is how to handle the incoming URL. Truth to be told, this is only one method: if you use SwiftUI, for example, you can handle incoming URLs with the onOpenURL function.

Note: I tried to make it work with the SceneDelegate as well, but I did not manage to. Therefore, I moved back from the SceneDelegate approach to the standard AppDelegate one. Despite that, from the Apple’s documentation, it should work in the same way for the SceneDelegate , but it doesn’t.

The relevant code is already isolated. First, notice that we are in the AppDelegate class, the entry point of our app.

To correctly handle our URL, we need to implement the application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool . In this example, this method delegates the whole computation to the handleURL function, if we ever need to reuse that piece of code somewhere else.

The handleURL function leverages the NSURLComponent to extract the path of the URL. If it contains /add or /rem , it handles the URL, by adding or removing a fixed amount of water. In that case, it also updates the VC and returns true . Otherwise, it simply returns false .

The Widget

Widgets can be created in our applications by:

Clicking on File > New > Target Select Widget Extension Add a name for the widget Press Finish

Xcode will then create a new folder and a new Target for the widget. There are three components that we have to implement:

a TimelineEntry , which contains the data to render, a TimelineProvider , which creates the TimelineEntries, the View , that has to render the data.

Luckily, Xcode generates some scaffolding for us, making it easier and faster to develop our widget.

The DrinkitEntry

In our sample, we have created a DrinkitEntry that is used as a model by SwiftUI . Here is the code:

This structure is a classic ViewModel : it contains some data that we want to render in the widgets and a date property that is required by the TimelineEntry to control when the Entry has to be updated.

The Provider

Entries are created by a Provider , thus we have to implement it as well. Here is the code for the provider:

The TimelineProvider protocol requires us to implement three different methods: placeholder(in:) , getSnapshot(in:completion:) , and getTimeline(in:completion:) . The output of all these methods is to return different versions of our data. The placeholder(in:) and the getSnapshot(in:completion:) are used to retrieve dummy versions of our data that are used in different situations by the OS. The method that creates the actual data is getTimeline(in:completion:) . In our example, we create a single entry, which has to be updated immediately (as the date: Date() and the policy: .atEnd parameters suggest).

Notice that every time we create a DrinkitEntry (lines 5, 10, and 18) we read the waterDrunk amount from the Storage , retrieving the most updated value available.

The WidgetView

Finally, the widget view. This is a classic SwiftUI view: we can use all the components of SwiftUI but the controls. So, no Button for example. Here, you can find a list of controls we can use in our Widgets.

We have two ways to implement actions in our Widgets:

by using the widgetURL modifier (this can be used in any kind of widget).

modifier (this can be used in any kind of widget). by using the Link element (this can be used only in medium and large widgets).

Here is the code for the widget:

The code is a bit lengthy. However, it should be quite easy to follow: we have the WidgetView on top and we have the three SwiftUI views we can use to render the three different widgets, at the bottom.

The really interesting lines are:

line 3, where we inject the widgetFamily to understand which widget we have to render.

to understand which widget we have to render. line 9, where we actually switch over the widgetFamily .

over the . line 30, where we add the widgetURL modifier. As you can see, it is as easy as creating a URL, by using the protocol we created with the URL Types .

modifier. As you can see, it is as easy as creating a URL, by using the protocol we created with the . lines 48, 56, 85, and 90, where we use the Link component to create a “button”, using similar URLs to the ones used at line 30.

And that’s basically it. We can now test our implementation. But how can we add a widget in the simulator?

Build and run the main app. Switch the target to the Widget extension. Build and run the widget extension. When the simulator pops up, keep any icon pressed. The icons will start moving exactly as if it was a real device. Tap on the + button in the top-left corner of the simulator: it should open a list of available widgets. Scroll the list until you find your widget in the gallery. Tap on it and choose the size you prefer.

Note: to support multiple families, remember to add the .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) at the end of the Widget StaticConfiguration ’s body property

And there it is, the result of our work!

Conclusion

In today's article, we rapidly saw how to implement a widget in SwiftUI on iOS 14. Not only that, we hack around to implement some real functionality in our app: widgets can be used not only to display data but also to implement some small features.

The whole codebase of the project can be found at this link.

Now, you can use this knowledge to provide stunning features to your user. The only downside of this is that the app is opened every time the user interacts with the widget. However, there is no way, at the moment, to prevent this from happening. But, it is not necessarily a bad thing: you can always leverage this moment to present some amazing offers to your users!