Reading Time: 9 minutes
On the Android platform, widgets have been there for ages. Widgets on Android are interactive mini-apps or extensions of a larger app. For example, you can display the weather overview, a mini music player on the phone’s home page, or an interactive background.
With iOS 14, Apple finally introduced widgets to their platform. However, unlike Android, the devices on iOS are static and will only display relevant information to the user. There is no kind of interaction, animation, or input.
As stated on the Apple website, widgets display glanceable relevant information from an app. Widgets allow users to open the related app for more details quickly. An app can provide multiple customizable widgets with each their unique look, layout, and information. For example, you can display the latest news in a list or display a calendar, but interactive mini-apps, such as a media player, are not supported.
Overall, a widget can add to your overall app experience, hopefully triggering users to open your app more often.
This blog post goes into the basic setup of a widget on iOS.
We first need to build a super simple news app to get started. In this case, it will be a list of news items retrieved from NewsAPI.org. The final result will be a news list displayed in a widget. Code will be shared between the widget and the real app.
1. Create a simple app to get started
Boot up Xcode and create an App project. Make sure SwiftUI is enabled.
If you run this app, it will display a content view with “Hello world” in it.
2. Create an Account on NewsAPI.org
In order to create the datasets and retrieve the data, we need an account on NewsAPI.org. So visit the website, create an account and get the API key.
3. Setup the dataset
On NewsAPI.org, take note of the following JSON:
Basically NewsAPI will return a list of articles containing the following:
– A status
– Number results
– The articles
This can all be mapped to a struct. So create the following file and fill in the following:
By complying to Codable, this makes it possible for the JSONDecoder to map the JSON to the struct later on when using the API Service.
Of course this also means we need to map the articles to a struct. The articles array inside of the list, have the following JSON.
This can all be mapped to a struct. To create the following file and fill in the following:
The Identifiable protocol is needed for the list view, we will be using the title as an ID for now.
In order to retrieve the data from News API, we need a simple API service which will retrieve the JSON and convert it to a Codable Dataset. So create the following:
In general, this simple class will try to retrieve the data and convert it to our Codable models.
The architecture and setup of the news app
For this example, we will be using a simple MVVM architecture with some Redux properties. For that, we create a view model for retrieving the data from News API with an observable state, which will contain the data.
So add a new file to the project and name it view model and fill in the following code:
In this class, we have an observable state, in which a subscriber can figure out which state ViewModel is in.
The Constants enum will contain one of the example URLs of NewsAPI.org and uses categories for filtering.
Please make sure to fill in your API key into the Constants enum before running this code.
ViewModel will have different observable states. This can be `loading` in which case it tries to retrieve the data. Or it can be success, in which case the ViewModel has retrieved a dataset, or it can have the error state.
When using this class and calling the getDataIfNeeded method, the ViewModel will automatically retrieve the JSON from NewsAPI.org and convert it to `NewsArticle`-models. These models can then be used to populate the view.
The getDataIfNeeded method also has functionality for filtering based on category.
In order to use this class, the view needs to have ViewModel as an observable object and the view needs to call the getDataIfNeeded() method upon viewAppear or on pull-to-refresh.
Modify the ContentView to the following code:
Setup of the Widget
If everything went well and the News app is displaying a list of NewsArticles it is time to add the widget.
Go to targets and add the ‘Widget Extension’, give it a name and make sure to activate the scheme
You will end up with a new directory in your project which contains the Widget classes you’ll need:
– The Widget main class (MyWidget.swift)
If you run the widget scheme, the simulator will now display a simple time widget.
For now, we only need to check the main Widget Swift file. We will get back later to the IntentDefinition.
The widget code looks like this:
How do widgets work?
Widgets work using a timeline. The timeline is visible inside of the ‘Provider struct’.
The timeline can be best described as a card deck, in which iOS will just grab and display the next card on top and discard the previous card after a certain amount of time. Each item in the timeline object is an entry that contains data and a date. The data is the information displayed to the user and the date is the position on the timeline. The timeline also contains a refresh policy. This can be either ‘never’, ‘at the end’ when the widget has processed and displayed all the data on the timeline or after a ‘certain date’.
To display the timeline entry, there is an ‘entry-view’, which is the content.
Other methods such getSnapshot and placeholder are used for the iOS configuration flow or when displaying the widget in its initial state.
For our example we will use only one entry which will be a list of news article titles and we will refresh once every 15 minutes.
Configuring the widget to display news items
o in order to get our news data into the widget extension we will be needing to add the news classes to the widget extension.
For the following classes, go to the inspector and add the classes to the widget target.
Now your app code is shared with the widget which allows us to use them in the widget.
Adjusting the news app classes
Since widgets are static and the OS is responsible for updating them, we cannot use ObservedObjects inside of them. We need to modify the ViewModel, so a widget is able to retrieve the data correctly.
Modify the ViewModel as follows:
By adding a completion handler to getDataIfNeeded method, we should be able to get the data without observing a property.
Create the following struct for the NewsEntry.
This will be our entry which will be placed into the timeline. In general we have multiple states.
For the placeholder and the getSnapshot we will be using the idle state. This state is a neutral state when there isn’t any data downloaded yet.
The success state will be displaying the news articles and the error state will display an error in the widget when something went wrong.
Since widgets are static and relatively small, we also want to limit the number of articles to a maximum of 4.
Update the provider code to the following
As seen above, the viewModel has been added as an object and will retrieve the data set. As for placeholder and getSnapshot, both will set an idle state. Notice that in the timeline method, the articles will be limited to a max of 4 and that, when something goes wrong, an error entry will be injected. Also notice that the timeline refresh policy will update after 15 minutes.
Implementing the widget view
Modify the WidgetEntryView so it looks like this:
Now the widget will render the news item as a list. It will also handle the different states of the entry.
However, this code will not run or render properly. The reason is that the text might be too large for the widget and we might want to support only one or two sizes. For this update the Main Widget Struct and the preview:
Build and run your widget extension project.
Congrats, you got your first simple widget working!
IntentDefinition adding widget configuration
Each widget has a backside which can be used to configure the widget. Setting this up is easy.
First open your MyWidget.intentdefinition. In the IntentDefinition edit screen, click on the small plus sign at the bottom left corner. Select enum and name it Category.
Now we are going to map the enum from our viewModel to the IntentDefinition, so we can apply a query filter to our widget.
Add the following cases to the enum:
Great now we have an enum, but we also need to add it to the IntentDefinition as a property. For that, while on the IntentDefinition edit screen, click on configuration.
In the configuration screen of the IntentDefinition, add a property named category and make sure it uses the just created enum as a ‘Type’.
Now compile your app and widget by running build, but do not run it yet.
Now navigate to the getTimeline function of the IntentTimelineProvider class. Notice that the configuration (ConfigurationIntent) object has a new property, named category. Now this can be used as a filter for retrieving the data. Modify the timeline function so it looks like this:
Now the timeline will get the news items using a filter.
To test this, build and run the app and widget.
When the widget appears, long press it and select edit.
Notice that it will flip and display a button in which you can select something.
Select a category and tap outside the widget. Notice that the widget will retrieve a different dataset based on the filtering you selected.
Finally, lets style the widget a little bit.
Styling the widget
Our widget looks a bit boring, so let’s improve a couple of things. Add the following struct to the widget extension project.
This view will handle the downloading of the images. As widgets do not support async downloaded images, you need to create something custom. This struct is one way of downloading the images.
What will happen in this struct is that the NetworkImageView will try to download the image data if the url is valid. After that it will display the image in an ImageView. If this fails it will display a ‘No Image’-text.
Adjust the entry view so it looks like this:
Compile and run your widget to see the result. It should display an image and a title which have been limited to 2 lines per news entry.
Congrats! This is your first widget!