In my last article, I have shown you how to create a widget with static options. This might be good for some cases, but in most cases, we might not be able to determine the widget’s options during development time. Under these circumstances, we will need a configurable widget with dynamic options.
In general, here are the situations where dynamic options are needed:
- The options are provided by a remote server (API).
- The options are from the widget’s host app.
- The options are generated at runtime.
In this article, I will walk you through the process of displaying dynamic options that are fetched from a remote server. Although the guide focuses on this scenario, the principles and techniques discussed can also be applied to the other 2 situations as well.
Note:
If you’re unfamiliar with the basic of creating a configurable widget , I highly encourage you to first read my blog post called “How to Create Configurable Widgets With Static Options?“.
The Crypto Price Widget
The sample widget we going to create is a customizable crypto price widget. It allows users to track their preferred cryptocurrencies.
The widget will fetch the market’s current top ten cryptocurrencies from the CoinCap API and show them as user selections. Once the user selected an asset, the widget will fetch the asset’s latest price from this API and show it on the widget.
As you can see, the selections (customization) given by this widget are from an API, which makes the widget’s options dynamic. Hence, creating this widget will involve setting up a custom type and adding an intents extension.
Let’s take a look at them in details one by one.
Adding Custom Type to Intent Definition
Assuming you already created the widget extension named “CryptoPriceWidget”. Open the intent definition file and add a new type. We will name our custom type Crypto
and give it 2 properties: name
and symbol
, both of them will be of type String
.
Notice that identifier
and displayString
are mandatory for a custom type in the intent definition, so we will just leave it there.
Next, select the custom intent and add a new parameter. Set the parameter type to Crypto
and also make sure to check the “Options are provided dynamically” checkbox.
With that, we have added a custom type to the intent definition.
Adding the Intents Extension
Setting up the Intents Extension
In order to show dynamic options, we must let WidgetKit know where to get data for the dynamic options. This is where intents extension comes into play. Let’s go ahead and add one to the targets:
After that, select the intents extension, go to the “General” tab, and then add the custom intent class name to the “Supported Intents” section. You can refer to the image below for more information:
Note:
If you’re having trouble opening the auto-generated class, try building the project by pressing ⌘B. This should trigger the appearance of a small arrow button (as shown below) that you can use to access the class.
Next up, select the “CryptoPriceWidget” intent definition and make it a target member of the intents extension.
This step is important as we will have to conform the IntentHandler
to the intent handling protocol generated by the intent definition.
Speaking of the intent handling protocol, here’s how to see its definition:
What we really interested is the protocol name. Go ahead and copy the protocol name CryptoPriceConfigurationIntentHandling
(the name might vary depending on how you name the custom intent).
Now, open the IntentHandler.swift
file located in the intent extension folder and add the conformance to the IntentHandler
class.
Implementing the Intent Handler
At this stage, you should see the compiler complaining about “Type ‘IntentHandler’ does not conform to protocol ‘CryptoPriceConfigurationintentHandling“. Go ahead and can ask Xcode to fix the issue for us.
As a result, you should see 2 protocol stubs being added:
func provideSelectedCryptoOptionsCollection(
for intent: CryptoPriceConfigurationIntent,
with completion: @escaping (INObjectCollection<Crypto>?, Error?) -> Void
) {
// Implementation here...
}
func provideSelectedCryptoOptionsCollection(
for intent: CryptoPriceConfigurationIntent
) async throws -> INObjectCollection<Crypto> {
// Implementation here...
}
Both of these methods serve the same purpose, but one uses closure-based syntax while the other utilizes the Swift Concurrency syntax. We only need to implement one of these, so go ahead and delete the closure-based one.
As the name implies, this method will provide a collection of cryptocurrency options for users to choose from. In other words, code that fetches the top ten cryptocurrencies from the remote server will go into this method.
Also, notice the return type of the method — INObjectCollection<Crypto>
, the Crypto
type being used here is the custom type we added at the beginning of this article.
OK, enough with the explanation. Let’s take a look at its implementation and break it down after:
func provideSelectedCryptoOptionsCollection(
for intent: CryptoPriceConfigurationIntent
) async throws -> INObjectCollection<Crypto> {
// 1
// Fetch list of top ten crypto from API
let assets = try await AssetFetcher.fetchTopTenAssets()
// 2
// Transform `[Asset]` to `[Crypto]`
let cryptos = assets.map { asset in
let crypto = Crypto(
identifier: asset.id,
display: "\(asset.name) (\(asset.symbol))"
)
crypto.symbol = asset.symbol
crypto.name = asset.name
return crypto
}
// 3
// Create a collection with the array of cryptos.
let collection = INObjectCollection(items: cryptos)
// Return the collections
return collection
}
- Fetch the top ten cryptocurrencies from API using the
AssetFetcher
helper class. - Transform the results we get from the API call (
[Asset]
) into our desired type ([Crypto]
). Do take note that thedisplayString
we provide when initializingCrypto
will be shown on the widget’s configuration UI. - Create and return a collection of
Crypto
so that WidgetKit can populate the dynamic options accordingly.
And with that, the setup process for the intents extension is complete.
Implementing the Widget
In terms of implementing the widget, there is nothing new to discuss, all the required principles and techniques have been discussed in the following articles:
- How to Update or Refresh a Widget?
- How to Fetch and Show Remote Data on a Widget?
- How to Create Configurable Widgets With Static Options?
Therefore, I’ll leave that as an exercise for you! In case you are stuck, you can find the full sample code here (files are located in the CryptoPriceWidget and CryptoPriceIntentExtension folder).
Wrapping Up
Now that you’ve learned how everything operates, showing dynamic options, whether they’re provided by the host app or generated at runtime, should be straightforward. All you need to do is to update the IntentHandler
implementation accordingly.
Just in case you are not sure how to share data between the host app and the widget extension, this article should get you started.
If you enjoy reading this article, feel free to check out my other articles related to WidgetKit. You can also follow me on Twitter, and subscribe to my newsletter so that you won’t miss out on any of my upcoming iOS development-related articles.
Thanks for reading. 👨🏻💻
👋🏻 Hey!
While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.
- ✨ Bartender: Superpower your menu bar and take full control over your menu bar items.
- ✨ CleanShot X: The best screen capture app I’ve ever used.
- ✨ PixelSnap: Measure on-screen elements with ease and precision.
- ✨ iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.