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

How to Create Configurable Widgets With Static Options?

The main idea of widgets is all about giving users quick and easy access to the information that is most relevant to them. To bring this idea to the next level, we can make a widget configurable, which allows users to select what data is displayed and how it’s shown on the widget.

When it comes to the widget’s configurable items, we can classify them into two categories:

  • Static options: The configurable items are static and can be generated during development time, such as a static list of colors.
  • Dynamic options: The configurable items might vary or are generated dynamically during runtime, such as a list of countries provided by a remote server.

In this article, we will be focusing on configurable widgets with static options. You will learn how to set up a configuration intent, implement the intent timeline provider and configure a configurable widget. There’s a lot of ground to cover. Let’s dive in.


The MyText Widget

To keep things as simple as possible while effectively showcasing how to create a configurable widget, I will use the following sample widget I created specifically for this article:

The sample widget (MyText Widget)

Let’s call this widget MyText widget. It is basically a widget that shows whatever text you desire using the given font size and border color.


Creating Widget Extensions with Configuration Intent

Just like creating a non-configurable widget, the first step is to create a widget extension. Make sure that the “Include Configuration Intent” checkbox is checked before hitting the “finish” button.

Adding widget extension with configuration intent in Xcode
Include configuration intent when creating widget extension

Once done, you should see an intent definition file being created. This is where we will define the widget configurations (more on that later).

An intent definition file in Xcode
The intent definition file

In case you are updating an existing widget, you can manually add an intent definition file to your widget extension. Go to File New File and choose the “SiriKit Intent Definition File”.

Manually adding a new intent definition file to the widget extension to Xcode
Manually adding a new intent definition file to the widget extension

Lastly, make sure that the intent definition file is a member of the main app target and the widget extension target.

Setting the intent definition file target membership in Xcode
Setting the intent definition file target membership

Setting up the Configuration Intent

With the widget extension in place, go ahead and open up the intent definition file and change the custom intent name to “MyTextConfiguration” so that it makes more sense. After that, make sure the category is set to “View” and “Intent is eligible for widgets” is checked. Leave other options unchecked as they are not related to widget configuration.

Setting up a widget's configuration intent in Xcode
Setting up the configuration intent

Next up, head over to the parameters section and add the desired parameters. You can consider each parameter as a separate configurable item of the widget. For the MyText widget, we will have 3 parameters.

The first parameter is myText, it represents the text being displayed on the widget. You can configure the parameter following the image below:

Adding string parameter to configuration intent in Xcode
Add parameter: myText

Keep in mind that the “Display Name” field will determine the text being displayed on the widget’s configuration UI.

The next parameter to add is fontSize. We will set its type to “Integer” and select “Stepper” as its input type.

Adding integer parameter to configuration intent in Xcode
Add parameter: fontSize

The last parameter to create is borderColor, but before that, let’s define an enum to represent the available colors for selection.

Adding enum to configuration intent in Xcode
Adding new enum

After renaming the enum to BorderColor, go ahead and add a red case:

Adding enum cases to configuration intent in Xcode
Adding enum cases

With the same idea, proceed to add the green and blue case accordingly.

Once done, we can head back to MyTextConfiguration, add a parameter called borderColor and set its type to “Border Color”.

Adding enum parameter to configuration intent in Xcode
Add parameter: borderColor

And with that, we’ve completed the setup of the configuration intent.

Viewing Auto-Generated Files

After finishing setting up the configuration intent, if we proceed to build the project (using ⌘B), Xcode will automatically generate the necessary classes and enums for us. We’ll need those later on when we implement the widget.

For the MyText widget, 2 files are auto-generated: MyTextConfigurationIntent.swift and BorderColor.swift. The content of these files is not important, but in case you want to take a look, you can do it as follow:

Viewing the auto-generated files for widget's configuration intent in Xcode
Viewing the auto-generated files

Implementing the Configurable Widget

With the configuration intent in place, it is time to work on the implementation. Just like implementing a non-configurable widget, the components we need to implement are:

  1. Timeline entry
  2. Widget view
  3. Timeline provider
  4. Widget configuration

Timeline Entry & Widget View Implementation

Let’s define the timeline entry based on the configurable parameters — display text, font size, and border color:

struct MyTextEntry: TimelineEntry {
    let date: Date
    let displayText: String
    let fontSize: Int
    let borderColor: Color
}

With that, we can implement the widget view like so:

struct MyTextWidgetView: View {
    
    let entry: MyTextEntry
    
    var body: some View {
        Text(entry.displayText)
            .font(.system(size: CGFloat(entry.fontSize)))
            .padding()
            .overlay(
                RoundedRectangle(cornerRadius: 6)
                    .stroke(entry.borderColor, lineWidth: 2)
            )
            .containerBackground(for: .widget) { }
    }
}

Timeline Provider Implementation

Be sure to import the Intent module before implementing the timeline provider.

import Intents

We will name the timeline provider MyTextTimelineProvider, and it has to conform to the IntentTimelineProvider protocol.

struct MyTextTimelineProvider: IntentTimelineProvider {
    
    // Implementation here ...
    // ...
    // ...
}

Here are the 3 methods we need to implement for the IntentTimelineProvider protocol.

func placeholder(in context: Context) -> MyTextEntry {
   
    MyTextEntry(
        date: Date(),
        displayText: "Hello! 👋",
        fontSize: 16,
        borderColor: .clear
    )
}

func getSnapshot(for configuration: MyTextConfigurationIntent,
                 in context: Context,
                 completion: @escaping (MyTextEntry) -> ()) {
    
    // Retrive user's configuration
    let myText = configuration.myText ?? "Hello! 👋"
    let fontSize = Int(truncating: configuration.fontSize ?? 16)
    let color = convertToColor(using: configuration.borderColor)

    let entry = MyTextEntry(
        date: Date(),
        displayText: myText,
        fontSize: fontSize,
        borderColor: color
    )

    completion(entry)
}

func getTimeline(for configuration: MyTextConfigurationIntent,
                 in context: Context,
                 completion: @escaping (Timeline<MyTextEntry>) -> ()) {
    
    // Retrive user's configuration
    let myText = configuration.myText ?? "Hello! 👋"
    let fontSize = Int(truncating: configuration.fontSize ?? 16)
    let color = convertToColor(using: configuration.borderColor)

    let entry = MyTextEntry(
        date: Date(),
        displayText: myText,
        fontSize: fontSize,
        borderColor: color
    )

    let timeline = Timeline(entries: [entry], policy: .atEnd)
    completion(timeline)
}

Unlike the non-configurable TimelineProvider implementation, the getSnapshot() and getTimeline() method will have an extra parameter — configuration, which we can use to retrieve the user’s configuration for our widget.

Note:

If you’re unfamiliar with implementing a timeline provider for a non-configurable widget, I highly encourage you to first read my blog post called “Getting Started With WidgetKit“.

Do take note that configuration is of type MyTextConfigurationIntent, which is the same as the class generated by Xcode to represent the configuration intent.

Using the auto-generated configuration intent class in code
Using the auto-generated class in code

Now, I would like to draw your attention to the code where we retrieve the user’s configuration:

let myText = configuration.myText ?? "Hello! 👋"
let fontSize = Int(truncating: configuration.fontSize ?? 16)
let color = convertToColor(using: configuration.borderColor)

As you can see, both configuration.myText and configuration.fontSize are optional. This is because Xcode auto-generated them as an optional type. As a result, we will have to give them a default value.

For the BroderColor enum we defined in the configuration intent, we will call the convertToColor(using:) function to convert it to SwiftUI.Color. Here’s how the function looks:

/// Convert `BorderColor` to `SwiftUI.Color`
private func convertToColor(using borderColor: BorderColor) -> Color {
    switch borderColor {
    case .red:
        return .red
        
    case .green:
        return .green
        
    case .blue:
        return .blue
        
    case .unknown:
        fatalError("Unknow color")
    }
}

The Widget Configuration Implementation

The final part of the puzzle is to configure our widget using IntentConfiguration and pass in the desired intent, which is MyTextConfigurationIntent:

struct MyTextWidget: Widget {
    
    let kind = "com.SwiftSenpaiDemo.MyTextWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(
            kind: kind,
            intent: MyTextConfigurationIntent.self,
            provider: MyTextTimelineProvider()
        ) { entry in
            MyTextWidgetView(entry: entry)
        }
        .configurationDisplayName("MyText Widget")
        .description("Show you favorite text!")
        .supportedFamilies([
            .systemSmall,
        ])
    }
}

Just like StaticConfiguration, we can also use the configurationDisplayName and description modifiers to further customize our widget.

The widget's configuration display name and description
The configuration display name and description

There you have it, we have successfully created a fully configurable home screen widget. You can get the full sample code here (all files are located in the MyTextWidget folder).


If you’re enjoying this article, consider checking out my piece on creating a widget with dynamic options. Additionally, you can also explore my other articles that cover various topics related to WidgetKit.

Be sure to 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.