WidgetKit is a framework which is introduced during WWDC 20, the first online-only developer conference. Using WidgetKit, developers are able to create beautiful widgets which can be displayed on the homescreen.

Widgets should be written in SwiftUI, which is fine because widgets are only available for iOS 14 and above. By writing widgets in SwiftUI, you’re also able to use Xcode Previews to preview every size of the your widget, which will improve your development speed.

New to SwiftUI? Read my blog post with an introduction to SwiftUI.

Widgets support dark mode by default, so it’s important to test your widget in both light and dark mode to be sure your widget is presented correctly.

About WidgetKit

Using WidgetKit, you’re able to create a widget based on two different types of configuration. The StaticConfiguration needs to be used when no configuration setting is needed from the user. If a configuration from a user is needed, you need to use a IntentConfiguration. For now, we only focus on the StaticConfiguration, as this is the simplest version to implement and a good way to start learning how to build a widget!

There are three different size styles for widgets, which are small, medium and large. By default all size styles are enabled, but you’re free to disable size styles. Apple uses an extra large widget size for Apple News, but this size isn’t available for developers.

New data for widgets is queried, which means you’re not full in control when you want to display new content inside your widget. The more your widget is shown, the more it will be reloaded. Reloads can be triggered using a background notification, timeline setup or an app-based action.

Scrolling in widgets is not supported, just as showing videos or animated images. The whole widget is a tap target, which can trigger a deeplink if needed. When creating a medium or large widget, you can set multiple tap targets including a deeplink.

Getting started…

A widget is an extension for an already existing project. If you don’t have an existing project yet, create a clean iOS project in Xcode.

Next, we can add the Widget Extension from the menubar ‘File’, ‘New’ and select ‘Target’. Search for ‘Widget Extension’ and press ‘Next’. Now you can give your widget a name and the setup is ready! Disable the checkbox for ‘Include Configuration Intent’, as we will create a widget based on a static configuration.

Adding a widget extension using Xcode.
Adding a widget extension using Xcode.

Run the widget target on a simulator and you’ll see that a small empty widget which is visible on the homescreen!

Building a widget with SwiftUI

After creating a widget using Xcode, the template code is already able to compile and show a simple widget containing the current time. As you may recognise when you compile the widget, the time will not update directly each minute, because the system decides when the widget may refresh.

Your widget is a struct that overrides the Widget class. It contains the basic configuration for your widget and looks similar like this:

@main
struct ZonneveldDevWidget: Widget {
    let kind: String = "ZonneveldDevWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            ZonneveldDevWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

In the code above, a new widget named ZonneveldDevWidget is created. The body contains the initialisation of the widget. A StaticConfiguration widget needs the following input:

  • Kind -> A unique identifier for the widget. You can fill in any identifier you want. You need this identifier if you want to reload your widget from code.
  • Provider -> The provider is needed to determine the timeline for refreshing your widget. This will be explained later in this blogpost.
  • Content closure -> The content closure contains the widget view that needs to be displayed to the user (for all sizes).

In the content closure, we have to return the content view of the widget, which is ZonneveldDevWidgetEntryView in the example. The view looks like:

struct SimpleEntry: TimelineEntry {
    let date: Date
}

struct ZonneveldDevWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

Visual example of the view inside a Widget:

Widget example for iOS
Widget example for iOS. Left on loading data, right when loading data is finshed.

The ZonneveldDevWidgetEntryView class is a SwiftUI view. If SwiftUI is new for you, please read my introduction to SwiftUI to learn more about SwiftUI, as this article won’t go in dept on building a view with SwiftUI.

For a widget, it’s common to use a TimelineEntry class to pass data to the view. The widget will first be visible with placeholder data, as shown on the left image. When the data is fetched, the widget will be refreshed and shown with the real data.

Loading your data with the placeholder view will happen automatically, so you don’t have to create a different view for this.

Fetching data for your widget

Now we understand how a widget view is created, it’s time to understand where we are able to fetch data for the widget.

Before we will fetch data, we should provide the widget some stubbed data, so it can already display a placeholder view for the widget to the users.

Code for generating a placeholder:

func placeholder(in context: Context) -> SimpleEntry {
   SimpleEntry(date: Date())
}

Above code allows us to display a placeholder view to the user. The placeholder function should contain stubbed data, so it could be displayed directly to the user. Fetching data inside a placeholder function is not possible.

For the placeholder view, we have to return a SimpleEntry object, which is the data object. In the example it only requires a Date object, but when you extend the SimpleEntry view with more data, you have to return more stubbed data.

Now you have a placeholder view, we can fetch data to finally display our widget. The function that needs to be updated, is getTimeline. Inside this function, you’re able to fetch data and also declare the next refresh moment of your widget.

The code of the getTimeline function will look this by default:

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

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

Inside the getTimeline function, you’re able to fetch data async. The widget will appear when you call the completion(timeline) function, so if you fetch data from the cloud, be sure you call the completion function in the network fetch complete function.

Also, think about an error state view when you’re not able to fetch data and should display an error to the user. For example, if the network request fails, you still should call the completion function and give the user feedback about what went wrong.

Data that is fetched, should be added to the array of entries. Each data should be equal to the data object entry the SwiftUI view is expecting. When you provide multiple entries, the system can refresh the data without the need to fetch new data.

Understanding the snapshot function

When a user wants to add your widget, they will see a snapshot version of your widget. You can decide which data should be visible inside this snapshot. The data displayed inside the snapshot can be fetched from the cloud before presenting it to the user.

Because you influence the content of, you’re able to show the perfect view.

The snapshot function will look like this:

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
}

Just like the timeline function, after the completion function is called, the snapshot will be displayed. Before this, the placeholder view is visible.

Support different sizes for your widget

You are able to recognise the size of the widget by defining an Environment for widgetFamily. After that, you are able to check the widget size style in the body. The size can be systemSmall, systemMedium or systemLarge.

Example:

struct ZonneveldDevWidgetEntryView : View {
    @Environment(\.widgetFamily) private var widgetFamily
    
    var body: some View {
        if widgetFamily == .systemSmall {
            Text("This is a small widget")
        } else {
            Text("This is a medium or large widget")
        }
    }
}

Xcode Previews

Using Xcode Previews, you’re able to view previews of your widget inside Xcode. It helps you a lot if you define previews for all sizes for light dark mode and dark mode (so 6 previews!).

A example of a preview is show below:

struct ZonneveldDevWidget_Previews: PreviewProvider {
    static var previews: some View {
        ZonneveldDevWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
            .previewDisplayName("Small widget")
            .environment(\.colorScheme, .dark)
    }
}

In this example, I created a preview with the WidgetPreviewContext set to systemSmall, which means the preview will display a small widget. This can also be se to systemMedium or systemLarge for other widget sizes.

Also the colorScheme is set to dark to get a dark mode preview, but you’re also able to set this to light (which is the default preview style).

Example of widget previews.

Conclusion

When creating a widget, you are full in control what to display inside the widget and how the widget should be styled. Widgets have a prominent location on the screen, which means it offers a large entrance to your app.

When creating widgets, Xcode Previews are very powerful to view all the possible sizes and styles, so you can quickly check how all sizes will look in both light and dark mode.

There is a lot more to learn about WidgetKit, like advanced refresh situations, setup an IntentConfiguration and more. I will write soon more about WidgetKit!

🚀 Like this article? Follow me on Twitter for more SwiftUI related blogs!

Share and Enjoy !

0Shares
Categories:iOS