• Widgets on iOS
  • Rakshith N
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: zhuzilin
  • Proofread: PassionPenguin, Zenblo

Apple recently added support for widgets to iOS. They allow users to access limited but useful information without accessing the application.

This article is intended to introduce widgets. In this article, we’ll explore the WidgetKit SDK extensively and walk you through the components and processes needed to build widgets. This article requires that you are already familiar with SwiftUI, because you will use it extensively when building widgets. As the widget is not a complete app by itself, it does not use app delegates or navigation stacks. In addition, widgets cannot stand on their own and need to rely on a parent application. In summary, widgets provide users with a snapshot of application information. The operating system will quickly trigger the widget to refresh its view at the time you set it.

Use requirement

First, before you can develop a widget, you need to meet the following criteria:

  1. Mac OS 10.15.5 or later.
  2. Xcode 12 or later, Xcode 12 link (you can use this link if you can’t update from the app Store, usually due to insufficient disk space)

The basic configuration

As mentioned earlier, widgets must depend on a parent application. So, let’s create a single-page application first. For the lifecycle option, I selected SwiftUI, which uses the @main attribute to determine the code entry. With the application built, we now need to add a Widget extension to hold the widget code.

Select File -> New -> Target -> Widget extension.

Give the widget a name and deselect ‘Include Configuration Intent’ for reasons discussed later.

Next click Finish and you will see a pop-up asking you to activate the Widget Extension Scheme. Click activate and the setup is complete.

Now, select the Swift file under the Widget extension, and you’ll see that Xcode has generated the skeleton of the code. Let’s take a look at the parts of the skeleton. First look at the Widget type structure, which will be the name of the Widget file that you enter during installation. This structure is preceded by the ‘@main’ attribute, indicating that this is the entrance to the widget. Let’s take a closer look at the individual configuration properties.

       StaticConfiguration(kind: kind, provider: Provider()) { entry in
           Static_WidgetEntryView(entry: entry)
               .frame(maxWidth: .infinity, maxHeight: .infinity)
               .background(Color.black)
       }
       .configurationDisplayName("My Widget")
       .description("This is an example widget.")
Copy the code

**Kind: ** This is the identifier of the widget and can be used to perform updates or queries. **Provider: ** This value is of type “TimelineProvider”, which is the data source for the widget and determines what data needs to be displayed at different points in time. ** This is the SwiftUI view that will be displayed to the user.

Note that WidgetConfiguration returns an instance of StaticConfiguration. There are actually two types of Widget configuration, static configuration and Intent configuration. The Intent configuration allows users to set up widgets at run time. In our current configuration (that is, static configuration), the widget displays static data, meaning that users cannot change the data displayed on the widget at run time.

Next, let’s talk about ‘SimpleEntry’. It is of type ‘TimelineEntry’ and is responsible for the data model of the widget. In our example, only one date parameter is used, and you can add other values as needed. For example, if you need to display text to the user according to some criteria, you can add text parameters here. In this structure, you need to implement the date parameter to provide the OS with different data timestamps.

Next comes the widget content, ‘Static_WidgetEntryView’ is where you can use your creativity. Keep in mind, however, that widgets are limited in size.

Widgets of different sizes are supported

WidgetKit supports small, medium, and three sizes. The ‘supportedFamilies’ option can be used at widget startup to determine which sizes you intend to support, and by default, all sizes are enabled.

supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
Copy the code

Since users are free to choose from three sizes of widgets, we need to design the best UI for each size of the widget. In the View file, we need to be able to determine which size the user selected and change the UI based on their choices. To do this, widget Kit provides an environment variable that returns the size of the widget selected by the user, and we can set up the UI based on it.

struct Static_WidgetEntryView : View {
    var entry: Provider.Entry

    @Environment(\.widgetFamily) var family

    @ViewBuilder
    var body: some View {

        switch family {
        case .systemSmall:
            ViewForSystemSmall(entry: entry)
        case .systemMedium:
            ViewForSystemMedium(entry: entry)
        case .systemLarge:
            ViewForSystemLarge(entry: entry)
        @unknown default:
            fatalError()}}}Copy the code

Time line of the Provider

The final piece of the puzzle in building our widget is a Provider. Provider is of type ‘TimelineProvider’, which implements three methods

func getSnapshot(in context: Context.completion: @escaping (SimpleEntry) - > ())
func getTimeline(in context: Context.completion: @escaping (Timeline<Entry>) - > ())
func placeholder(in context: Context) -> SimpleEntry
Copy the code

Of the three methods, one (placeholder) provides placeholders for widgets, one (getSnapshot) provides snapshots, and the remaining one (getTimeline) returns the current timeline.

Snapshots are used when the OS needs to return to the view as quickly as possible without loading any data or network communication. The Widget Gallery uses snapshots so that users can preview the widget before adding it to the home screen. The ideal snapshot is a mock view of the widget (mocked view).

The getTimeline function is used to tell the widget what to display at different times. A timeline is basically an array of objects that comply with the TimelineEntry protocol. For example, if you want to make a widget that displays the number of days counting down to a particular event, you need to create a series of views from now until the date that event occurred.

This is also where you can do asynchronous network communication. Widgets can retrieve data through network communication or through containers shared by host applications. After these communication calls are complete, the widget displays the retrieved data.

TimelineReloadPolicy the OS uses the ‘TimelineReloadPolicy’ to determine when widgets need to be updated to the next set of views. The “.atend “reload policy reloads new entries when there are no more timeline entries. In the sample code, I created a timeline with 1-minute intervals and added five view entries. In this way, the widget is updated to show the corresponding time every minute. After five minutes, the system calls the getTimeline method to get the next set of views. TimelineReloadPolicy also provides options such as “After (Date)”, which updates the timeline at a specified time, and “Never”, which sets not to update the timeline at all.

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date()}func getSnapshot(in context: Context.completion: @escaping (SimpleEntry) - > ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

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

        // The interval between entries is 1 minute
        let currentDate = Date(a)for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

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

Run the project, and on the home screen, long press and click the “+” in the upper left corner. Select your Widget application from the options, select the widget style you want to Add, and click Add Widget. At this point, you should see the widget display the current time.

Dynamic configuration of widgets

So far, our widget has been largely static, with users unable to interact with it or determine what the widget displays at run time. Using the Intent configuration, we will be able to make our widgets dynamic. In our initial project setup, we unchecked the “Include Configuration Intent” option to make the widget customizable, and now let’s look at how to make the widget more interactive.

In this demo, we will implement a widget that allows users to select from a list of cities.

Example Set a custom intent

1) We need to create a custom Intent Definition, for which we will use SiriKit Intent Definition. Click on the File menu option, select the New File option, and continue with “SiriKit Intent Definition”. Give it a name, for example I’ll call it CityNamesIntent.

2) Select the newly created intent file. We now need to add a new intent. To do this, click the “+” icon in the lower left corner, select New Intent, and name it CityNames. Next, under Custom Intent on the right, set the category to View, and make sure the Intent is eligible for Widgets is selected.

3) After adding a new intent, we need to define the properties that the intent will handle. For this example, a simple enumeration of city names will suffice. Click the “+” icon again and select New Enum. Click on the newly created enumeration to set its properties. In the Cases section, click the “+” icon to add a value to the enumeration, like the city name I added in the figure.

4) Finally, go back to the CityName Intent we created. In the parameters section, click the “+” icon at the bottom and add a new parameter named Cities. Provide the appropriate display name, and then select the CityNamesEnum we just created under Type.

Our custom Intent Definition is done. Next we need to make this intent accessible to our widget. To do this, select the intent we created in the Supported Intents section of Project Targets.

Now, we need to update the widget from a static configuration to an Intent configuration. To do this, let’s create a new Provider instance. Create a “ConfigurableProvider” of type IntentTimelineProvider and use the same three functions as TimelineProvider. The type of this new parameter is the CityNamesIntent we defined earlier. This new configuration parameter can be used to get user-selected values and update or modify the timeline accordingly.

struct ConfigurableProvider: IntentTimelineProvider {

    typealias Entry = SimpleEntry

    typealias Intent = CityNamesIntent

    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date()}func getSnapshot(for configuration: CityNamesIntent.in context: Context.completion: @escaping (SimpleEntry) - >Void) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(for configuration: CityNamesIntent.in context: Context.completion: @escaping (Timeline<SimpleEntry- > >)Void) {
        var entries: [SimpleEntry] = []

        let currentDate = Date(a)for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

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

The last thing we need to do is update the widget definition from StaticConfiguration to IntentConfiguration. In the Static_Widget definition section, replace StaticConfiguration with IntentConfiguration. It needs an intent instance, and it passes the CityNameIntent to it. For the Provider, use the ConfigurableProvider we created. The rest remains the same.

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

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind,
                            intent: CityNamesIntent.self,
                            provider: ConfigurableProvider(),
                            content: { entry in
                                Static_WidgetEntryView(entry: entry)
                                .frame(maxWidth: .infinity, maxHeight: .infinity)
                                .background(Color.black)
                            })
            .configurationDisplayName("My Widget")
            .description("This is an example widget.")
            .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
Copy the code

At this point, users can configure our widget. Run the app, hold down the widget and select “Edit Widget”, and you’ll see a list of the city names we’ve provided. After making any selection, you can get the selected value in the Provider and change the view accordingly.

That concludes this article, but hopefully you’ve learned the basics of widgets. Widgets provide users with a new way to use applications and bring greater possibilities to applications. I strongly encourage you to continue exploring other uses of widgets, such as deep linking.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.