You are currently viewing How to Create Configurable Widgets With Dynamic Options?

How to Create Configurable Widgets With Dynamic Options?

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:

  1. The options are provided by a remote server (API).
  2. The options are from the widget’s host app.
  3. 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.

Add new type to intent definition in Xcode
Add new type to intent definition

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.

Add parameter of custom type in Xcode
Add parameter of custom type

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:

Add intents extension as new target in Xcode
Add intents extension as new target

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.

How to add supported intent to intents extension in Xcode
Add supported intent

Next up, select the “CryptoPriceWidget” intent definition and make it a target member of the intents extension.

Make intent definition member of the intents extension target in Xcode
Make intent definition member of the intents extension target

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:

Locate the intent handling protocol definition in Xcode
Locate the intent handling protocol 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.

Conform to intent handling protocol using Swift in Xcode
Conform to intent handling protocol

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.

Fix the intent handling protocol conformance error in Xcode
Fix the intent handling protocol conformance error

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
}
  1. Fetch the top ten cryptocurrencies from API using the AssetFetcher helper class.
  2. Transform the results we get from the API call ([Asset]) into our desired type ([Crypto]). Do take note that the displayString we provide when initializing Crypto will be shown on the widget’s configuration UI.
  3. 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:

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.