Created By Ningyuan — 2020/09/27

First of all, to make fun of Apple, the press conference and official documents only demonstrated the normal process based on Swift project, while we OC project or mixed, have to step on the pit research.

IOS14 brings with it the new WidgetKit framework, which is the Extension enhancement that came with iOS10. The lowest supported version is iOS14, and the development language is SwiftUI.

  • Widgets come in only three sizes: systemSmall, systemMedium, and systemLarge

    Screen size (portrait) Small widget Medium widget Large widget
    414×896 pt 169×169 pt 360×169 pt 360×379 pt
    375×812 pt 155×155 pt 329×155 pt 329×345 pt
    414×736 pt 159×159 pt 348×159 pt 348×357 pt
    375×667 pt 148×148 pt 321×148 pt 321×324 pt
    320×568 pt 141×141 pt 292×141 pt 292×311 pt
  • An App can create multiple widgets with different functions or presentation styles, and each Widget can optionally be provided in one of the three sizes mentioned above

  • Like previous versions of Extension, widgets are standalone applications with their own life cycle (called Timeline) and are theoretically “independent” of the main project

  • Widgets can only refresh data through a preset Timeline, not in real time

The following sections, in conjunction with the Demo, talk about some of the essentials of Widget development

1. Create a non-configurable Widget

Skip the main project creation step here and start creating the Widget project directly

Create a new Target in theApplication ExtensionFound in theWidget Extension

After clicking next, fill the ProductName of the Widget with the Include Configuration Intent. Whether the Widget should use a StaticConfiguration or a user configurable IntentConfiguration, leave the box blank and start with the normal unconfigurable mode for the Widget

Normally, when you’re creating something for the first time, you’ll get this popover, so you can hit Activate

Xcode then creates a time-display Widget by default, running as shown in the figure below

You can also manually select widgets of other sizes and add them to your desktop

2. Basic code

Here’s a look at the base code that Xcode creates, slightly different from the Beta version

Provider

Provide refresh data, control data refresh related methods, there are three default methods

  • Placeholder method, which provides placeholder data to the widget and asynchronously returns a TimelineEtry
func placeholder(in context: Context) -> SimpleEntry {
	SimpleEntry(date: Date()}Copy the code

It looks something like this

  • The getSnapshot method provides the data required when the Widget first starts rendering or appears for the first time, which is generally considered initialization data
func getSnapshot(in context: Context.completion: @escaping (SimpleEntry) - > ()) {
	let entry = SimpleEntry(date: Date())
	completion(entry)
}
Copy the code
  • The getTimeline method, which calls back to receive an array of entries, can be thought of as a data source and refresh interval
func getTimeline(in context: Context.completion: @escaping (Timeline<Entry>) - > ()) {
	var entries: [SimpleEntry] = []

	// Add five entries with a refresh time of 1 hour for each entry
	let currentDate = Date(a)for hourOffset in 0 ..< 5 {
		let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
		let entry = SimpleEntry(date: entryDate)
		entries.append(entry)
	}

    // Here is the key point, the Entry and the refresh mechanism you want to set. AtEnd indicates that you can refresh the Timeline after the entries are displayed
	let timeline = Timeline(entries: entries, policy: .atEnd)
	completion(timeline)
}
Copy the code

SimpleEntry

In the simplest TimelineEntry, only a date variable is declared to record the entry time. The official interpretation is that WidgetKit renders the widget at the time

struct SimpleEntry: TimelineEntry {
    let date: Date
}
Copy the code

EntryView

The View part of the Widget that binds data to the View

struct WeatherWidgetEntryView : View {
    var entry: Provider.Entry

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

Widget

The widgets we created are not configurable, so a StaticConfiguration is automatically generated to provide configuration for statically typed widgets

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

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

Preview

The preview on canvas, which often goes wrong, feels negligible ~

struct WeatherWidget_Previews: PreviewProvider {
    static var previews: some View {
        WeatherWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}
Copy the code

3. Fit different sizes

  • Change EntryView to the following way, using @Environment provided by SwiftUI, you can display different layouts according to different sizes
struct MyWidgetEntryView : View {
    
    @Environment(\.widgetFamily)
    var family: WidgetFamily
    var entry: Provider.Entry

    @ViewBuilder
    var body: some View {
        
        switch family {
        case .systemSmall:Text(entry.date, style: .time)
        case .systemMedium: Text(entry.date, style: .time)
        case .systemLarge: Text(entry.date, style: .time)
        default:Text(entry.date, style: .time)
        }
    }
}
Copy the code
  • If you want to fit only a certain size, you can add it after Configuration.supportedFamilies, if not, it means that three sizes are used by default

4. Add configurable modes to widgets

Next, how to add a configurable mode to a Widget is described in two ways: Add a new project or add it on a static configuration basis

(1) Select Include Configuration Intent when creating a new Widget

There will be an additional XX.intentDefinition file in the new project

IntentConfiguration replaces StaticConfiguration in the code

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

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            WeatherWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")}}Copy the code

The getSnapshot and getTimeline methods in the Provider have a configuration parameter

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

    func getTimeline(for configuration: ConfigurationIntent.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(a)for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

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

Then add different Parameters in the Parameters, you can also add enumeration, custom type, etc., specific still need to explore by yourself

Then fetch the title for display

After the configuration is complete, run the Widget. Hold down the Widget to display a configuration Edit Widget. Tap to enter the Widget editing interface

The configuration page is displayed

complete

(2) Change the static configuration to dynamic configuration

This is a bit trickier than creating a widget and checking the Intent directly. First, we create an Intent Definition file within the widget project

The file name can be created at will. Remember Targets requires Extension

This is what happens when you create the Intents inside

Press + to select New Intent, as shown in the following figure

Give the Intent a name. Here I’ll use Configuration, which I’ll use later. Under the Custom Intent, modify the Category to View, and select Widgets, and set it to Work without any preset preset information or Shortcuts. Then add a content to Parameter

To change the Intent, add import Intents to the Widget’s Swift file, and replace it with a “ConfigurationIntent” Configuration. When Xcode generates the Configuration file, The Intent is appended at the end

To modify the Provider
  • The protocol to follow is IntentTimelineProvider
  • GetSnapshot and getTimeline also need to be modified accordingly. It is recommended to annotate the original method temporarily, type a few words, be prompted by Xcode and press Enter, confirm the modification before deleting the original code

I guess 99% of you will get an error at this step, so please go toFAQ Q&A

Modify SimpleEntry

Add a data variable of type ConfigurationIntent

Modify the Widget

The StaticConfiguration was changed to IntentConfiguration

If you need Preview, modify it as prompted by Xcode

FAQ Q&A:

Q1: An error is reported when you select Include Configuration Intent when creating a Widget

The class name of the Intent should also be prefixed, for example, ABConfigurationIntent

Q2: An error occurs when a new Intent is manually created

Common errors are: Type ‘Provider’ does not confirm to protocol ‘IntentTimelineProvider’ or Cannot find ‘ConfigurationIntent’ in Scope, ConfigurationIntent represents the custom configuration name of the IntentDefinition file

We’ll start with the ConfigurationIntent file, which is not present in our main project but generated each time we compile the Widget by checking the generated file path

Since the compile time error, means that the ConfigurationIntent file is not associated with our project, with no import this project is a bit like, so just can have always find a ‘XXXIntent’ in the Scope of errors

In the BuildSettings of the Extension Target, search for the intent. There is an Intent Definition Complier-code Generation property bar. The Intent Class Generation Language configuration is Automatic

If not, please confirm if the DefinitionIntent file has been added to the project

You can change it to Swift and then compile it. You can see that the cache file just generated OC file… No wonder compiling failed. Then we have the problem.

Automatic does not automatically generate the Intent configuration file based on the language used by the project.

OC project, create is OC file

Mix the project, create an OC file

The pure Swift project creates OC files

Check this option and change it to Swift to solve this problem

Q3: Q2 error, modify, use the simulator to run after clickEdit WidgetHave no reaction

This seems to be a bug, you need to delete the application in the emulator first, and then run again

References:

  • Widget to introduce the official document: developer.apple.com/documentati…
  • Widget official Demo:developer.apple.com/documentati…
  • Widget additional configurable mode: developer.apple.com/documentati…