Widgets have existed in iOS even before iOS 14, albeit they were known as “Today Extensions” rather than “Widgets”. With the release of iOS 14, Today Extensions were left behind in favor of Widgets which are more flexible and powerful.
These are some major differences between Today Extensions and Widgets:
- They can only be displayed in the Today View.
- Support up to 16MB.
- Few sizes.
- They can be displayed in the Today View and in the Home View.
- Support up to 30MB (Note: There is no official documentation about this, but Widgets seem to crash when they exceed 30MB)
- Relatively more sizes.
The Future of Today Extensions
So, what’s gonna happen to Today Extensions as Widgets have taken over their place?
In Xcode 12 you can no longer create Today Extensions from scratch, but if one had already been created with an older version of Xcode, it can coexist with the newer Widgets making it possible for users with iOS 13 to still use them.
Supported Families (Widget Sizes)
An iOS 14 Widget supports three different sizes. The difference in size between them can be visualized as the number of icons they would occupy when placed on the Home screen.
- systemSmall: Takes the space of 4 icons
- systemMedium: Takes the space of 8 icons
- systemLarge: Takes the space of 16 icons
Creating a Static Widget
Start by downloading the sample project from here. This project contains helper classes and methods for creating views and making API calls.
Open your starter project.
To create a new widget go to File > New > Target, then search for ‘Widget Extension’.
Configuring the Widget
There are two ways a widget can be configured as.
- Static Configuration: Widget with no user-configurable object
- Intent Configuration: Widget with a user-configurable object. It uses a custom intent definition to provide users with configurable options.
Name your widget
StatsWidget. Here we will be creating a Static Widget. So make sure, "Include Configuration Intent" is left unchecked.
This will create a bunch of boilerplate code.
Note: When you create a Widget, a new target will be added as well. Make sure that any file or resource used for the widget is included in this new target membership.
To include an object in the widget’s Target Membership. Select the file or resource you want to add and navigate to the `File Inspector` (View → Inspectors → File). Then check the widget’s target (`StatsWidgetExtension` in this case).
First, let's take a look at the
StatsWidget struct declaration. This struct defines the interface of the widget.
@main keyword indicates that this will be the entry point for the extension. A Widget has a body property that returns a WidgetConfiguration, in our case a StaticConfiguration. This type of configuration needs the following three things to define its content.
- A closure to display content
description are displayed in the interface presented to the user to add the widget.
We can create multiple widgets in one extension but each widget needs to have a unique kind. “Kind” is the string that identifies a widget when reloading it.
2. Timeline Provider
The “Timeline Provider” provides the necessary data for widgets to display. It needs to implement three methods and each method provides a timeline, snapshot, and a placeholder for the widget
Timeline is the core engine of a widget. The timeline contains an array of Timeline Entries that define what to display in the widget at a specific time. The framework saves the timeline in the widget and renders the view when the specified time comes. It also specifies the reload policy (i.e when to request for a new timeline.)
The Timeline Entry contains a date property that specifies the date of when to render a certain view in the widget. The other properties contain the necessary information that is to be displayed in the widget
Replace the content of
SimpleEntry with the following code. Here, stats contains the necessary information to display in the object.
Let’s look at the Timeline Provider.
Here, the associated value Entry defines that we will be using
SimpleEntry as our Timeline Entry.
Reloading the widget guarantees the user is provided with the latest information. There are three policies.
- atEnd: Request for a new timeline when the current timeline displays the last entry.
- after(date:): Request for a new timeline at a specific date.
- never: Do not request for a new timeline.
Replace the code inside the
In the following code, we create an array of entries and a reload policy for when to request a new timeline. We perform asynchronous tasks like calling the network. We read the necessary data from the API and provide it as a single entry. The reload policy is set so that it will request a new timeline after a day.
Snapshot is a view that will be displayed when the users intends to add the widget to the home screen. The Timeline Provider defines a single entry for a snapshot and returns it through a completion block. You can use the user data or dummy data so that the user can visualize how the widget will look like.
Replace the code inside the
The placeholder is a view that is displayed while your system is loading. It will replace your Text and Images with a faded rectangle. In Timeline, providers just need to provide the system with a single entry.
This is how a placeholder view looks.
Replace the code inside the
placeholder method to provide an entry.
3. A Closure to display content
The widget uses a SwiftUI View to display content. The closure takes an entry provided by the Timeline provider and passes it to the view to display.
Replace the code inside the
Finally, run the widget and you should be able to see the result.
Support a Specific Widget Family
By default, the widget supports all three sizes. But we can specify the widget to support only specific families. To do that we use the
With this, our widget will only support the small and medium sizes. You can confirm this from the Widget Library where you add new widgets. After setting this modifier only small and medium sizes should be available.
Change the Layout Depending on the Widget Family
At the moment the small widget and the medium widget have the same layout. But we can provide a different layout for each widget family. We need the new @Environment Property Wrapper
.widgetFamily to determine the current family of the widget and then return different layouts for different families.
Modify the EntryView code to the one below.
Here, we use a
switch statement on the
family property that holds the current widget family. It sets a specific layout for a small size and a default layout any other size. In this case, only the medium size will use the default layout since the widget itself is set to only supports two sizes. Run the widget. We should be able to see a different layout for the medium size.
Widgets are very limited in space displaying only a small amount of information. Users can tap certain content of the Widget or the Widget itself to open the app to display more information, also known as deep-linking. By default, clicking on a widget will simply open the app. But it is always good to display a screen with content related to the information displayed in the widget. There are two ways to specify a URL.
widgetURL(_ url: URL?): This will make a whole widget deep-link to the specific URL. Small Widgets can only use widgetURL as they only support one tappable area.
Link(destination: URL): This will wrap some content into a clickable deep link.
.widgetURL(URL(string: stats.country))to the end of the small widget layout. We are passing the name of the country as a URL to the app. When we receive the URL inside the app, we will look for the information about the country inside the array of the Country list and then display the
ContentView swift file inside the main
App Target folder and find the
onOpenURL callback. Here, after finding the information about the country, we will set
present = true which displays the
Supporting Multiple Widgets
You can add multiple widgets in a single extension by creating a
WidgetBundle to return multiple WidgetConfiguration objects. To do so, we need to create a
WidgetBundle and move the @main declaration to the widget bundle instead.
WidgetBundle also has a
body property which returns
some Widget. We can support multiple widgets by simply adding multiple configurations to the bundle.
Widgets are very useful for displaying a piece of information but they should not be used as a replacement for the app. Widgets have very limited space, so it’s not good to put too much information in them. Widgets should be simple relying on the app itself for any heavy tasks.