[iOS] record the widget development process and problems encountered

Posted by Froolah on Fri, 03 Dec 2021 15:52:04 +0100

Write in front

1. After iOS14, apple updated the extension component and introduced a new UI component: WidgetKit, but abandoned the Today Extension component of versions below iOS14;
2. The widgextension uses a new WidgetKit, which is different from the Today Widget. It can only be developed using SwiftUI, so SwiftUI and Swift foundation are required;
3. The Widget supports three sizes systemSmall (2x2), systemMedium (4x2) and systemLarge (4x4)

1. Create Widget

First, create a project named MyApp;
Then create the Widget, file - > New - > target

iOS -> Application Extension -> Widget Extension

Enter the project name;
Note that widgets are divided into Static and Intent modes. Long press in Intent mode to edit, but there is no editing option in Static mode. The following figure shows Intent mode:

It is named MyWidget. If it is not selected here, Include Configuration Intent will be created. If it is not selected, a static widget will be created. If it is selected, an editable widget will be created.
Note: you can't name Widget here. If the system has this file, an error will be reported.
You can see here

Click create and a pop-up window will pop up. Click Activate directly

Now you can run it, and a widget with only time text will be displayed on the simulator.

2. Structure description

There are five structures in the MyWidget.swift file

2.1,Provider

Where the timeline is managed, data binding processing.

2.2,SimpleEntry

The timeline entry produces only one date attribute by default. If you need to customize the field, it is declared here.

2.3,MyWidgetEntryView

The widget view layout is implemented here, and the processing data is displayed to the view.

2.4,MyWidget

Widget loading entry, static calling StaticConfiguration, editable calling IntentConfiguration.

2.5,MyWidget_Previews

View preview.

Here is a more detailed introduction: iOS 14 WidgetKit development

3. Change Static to Intent

3.1. Add Intent file

If we need to change the static to editable, we need to manually add the Intent file.
File -> New -> File (cmd + N)
iOS -> Resource -> SiriKit Intent Definition File

It is named Custom.intentdefinition. Here, small and medium components should be selected for Targets, which is not selected by default.

3.2. Add Intent

Name MyIntent, Category, select View, select user confirmation required, and none.
After adding, compiling the project (cmd + B) will generate a myintentint.swift file. MyIntent is the name of our custom Intent.

File path

3.3. Modify MyWidget.swift

3.3.1,Provider

Change TimelineProvider to IntentTimelineProvider

struct Provider: TimelineProvider {
...
Change to
struct Provider: IntentTimelineProvider {
...

getSnapshot method modification

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
...
Change to
func getSnapshot(for configuration:MyIntentIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
...

getTimeline method modification

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
...
Change to
func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
...
3.3.2 SimpleEntry

Add configuration attribute

let configuration: MyIntentIntent

Modify where SimpleEntry is called

Provider -> placeholder
SimpleEntry(date: Date(), configuration: MyIntentIntent())

Provider -> getSnapshot
let entry = SimpleEntry(date: Date(), configuration: configuration)

Provider -> getTimeline
let entry = SimpleEntry(date: entryDate, configuration: configuration)

MyWidget_Previews
MyWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: MyIntentIntent()))
3.3.3 MyWidget

Change StaticConfiguration to IntentConfiguration

StaticConfiguration(kind: kind, provider: Provider()) { entry in
...
Change to
IntentConfiguration(kind: kind, intent: MyIntentIntent.self, provider: Provider()) { entry in
...
3.3.4 MyWidget_Previews

The above has been modified, see 3.3.2.

There is a problem here, because it is statically modified to editable. Clicking Edit will not bring up the editing interface. Delete the widget and add it again.
Add an option and try it.

Get the parameter value in MyWidgetEntryView and display it on the screen

Text(entry.configuration.parameter ?? "")


4. Custom optional list


4.1. Create IntentHandle

Add Target: IOS - > Application Extension - > intents extension

Named intenthandle. This binding Intent file and data model, set up the next target to select the small components, it can be called in the group, Intent target to select Intenthandle. The MyWidget.swift file does not move.

4.2. Create a list file and add picture resources

Create the MenuJson.swift file. Note that the widget and handle are selected for target

Add picture

import Foundation

struct MenuJson: Codable {
    let id: String
    let name: String
    let image: String
    
    static func createMenuList() -> [MenuJson] {
        var list = [MenuJson]()
        list.append(.init(id: "1", name: "Aerial photography", image: "aerial.jpg"))
        list.append(.init(id: "2", name: "city", image: "city.jpg"))
        list.append(.init(id: "3", name: "character", image: "figure.jpg"))
        list.append(.init(id: "4", name: "Pets", image: "pet.jpg"))
        return list
    }
}

Here, select the target of small and medium-sized components

4.3. Add data to the Intent file

4.3.1. Add Type

Add a Type attribute, select Add Type... And the Type is named MenuList.

Type display name can be customized

4.4. Modify IntentHandler file

Add agent myinthandling
Add method provideTypeOptionsCollection
Binding data in provideTypeOptionsCollection

class IntentHandler: INExtension, MyIntentIntentHandling {
    
    func provideTypeOptionsCollection(for intent: MyIntentIntent, with completion: @escaping (INObjectCollection<MenuList>?, Error?) -> Void) {
        let list = MenuJson.createMenuList().map { (item) -> MenuList in
            .init(identifier: item.id, display: item.name)
        }
        completion(.init(items: list), nil)
    }
    ...

4.5. Modify MyWidget file

struct Provider: IntentTimelineProvider {
    
    let list = MenuJson.createMenuList().map { (item) -> MenuList in
        .init(identifier: item.id, display: item.name)
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(
            date: Date(),
            configuration: MyIntentIntent(),
            menu: list[0]
        )
    }

    func getSnapshot(for configuration:MyIntentIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        
        var firstItem = list.filter { (item: MenuList) -> Bool in
            item.identifier == configuration.type?.identifier
        }
        if firstItem.count == 0 {
            firstItem = list
        }
        
        let entry = SimpleEntry(
            date: Date(),
            configuration: configuration,
            menu: firstItem[0]
        )
        completion(entry)
    }

    func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        
        var firstItem = list.filter { (item: MenuList) -> Bool in
            item.identifier == configuration.type?.identifier
        }
        if firstItem.count == 0 {
            firstItem = list
        }
        
        let entry = SimpleEntry(
            date: Date(),
            configuration: configuration,
            menu: firstItem[0]
        )

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

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: MyIntentIntent
    let menu: MenuList
}

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        
        let item = MenuJson.createMenuList().first { (subItem: MenuJson) -> Bool in
            subItem.id == entry.menu.identifier
        }
        let defaultText = "Sword dancing Hongmen can forgive the Han Dynasty, the ship sank, and the giant deer competed to kill the Qin Dynasty.\n-- clear • Yan Suicheng"
        ZStack {
            
            Image(uiImage: UIImage(imageLiteralResourceName: item?.image ?? "figure.jpg"))
                .resizable()
                .aspectRatio(contentMode: .fit)
            VStack {
                Spacer()
                Text(item?.name ?? defaultText)
            }
            
        }
        .widgetURL(URL(string: "widget://tap"))
    }
}

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

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

struct MyWidget_Previews: PreviewProvider {
    static var previews: some View {
        let list = MenuJson.createMenuList().map { (item) -> MenuList in
            .init(identifier: item.id, display: item.name)
        }
        MyWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: MyIntentIntent(), menu: list[0]))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

Note:
The pictures added by SwiftUI Image("") can only be placed in Assets. The pictures directly dragged into the project need to call UIImage to load

Image(uiImage: UIImage(imageLiteralResourceName: item?.image ?? "figure.jpg"))

Topics: Swift iOS xcode