今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

Step-by-step guide to creating widget2

Step-by-Step Guide to Create Widget2#

Following the previous article on iOS Widget, this article introduces the usage of WidgetBundle and how to create a widget similar to Alipay. In the previous article, WidgetBundle was mistakenly referred to as WidgetGroup, my mistake.

Usage of WidgetBundle#

Let's review when to use WidgetBundle. The previous article introduced supportedFamilies, which allows setting different sizes for widgets, such as Small, Medium, Large, etc. However, if you want multiple widgets of the same size, for example, two Small size widgets, similar to the effect of the Dongfang Caifu widget below, you need to use WidgetBundle to set multiple Widgets.

image image image

Using WidgetBundle is not difficult. Let's take a look at the last code from the previous article (you can download it from https://github.com/mokong/WidgetAllInOne, open Tutorial2), which only displayed one Medium size widget. Here we modify it to use WidgetBundle to display two Medium size widgets.

Create a new SwiftUIView named WidgetBundleDemo, with the following steps:

  • Import WidgetKit
  • Change the main entry to WidgetBundleDemo
  • Change the WidgetBundleDemo type to WidgetBundle
  • Change the body type to Widget

The code is as follows:


import SwiftUI
import WidgetKit

@main
struct WidgetBundleDemo: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        DemoWidget()
        DemoWidget()
    }
}

Then compile and run, it failed with the error xxx... error: 'main' attribute can only apply to one type in a module, meaning that there can only be one @main marking the program entry in a module. Therefore, we need to remove the extra @main. Where is it? In DemoWidget.swift, because the previous main entry was DemoWidget, and now the main entry is the newly created WidgetBundleDemo, so we need to remove @main from DemoWidget. After removing it, run again to check the effect and find that two identical Medium size widgets appear in the widget preview.

Wait, it was mentioned in the previous article that when swiping left and right between different widgets, the title and description above will also slide along. Why doesn't it slide here?

Indeed, it should be the reason that the title and content are the same. Let's verify this. First, add title and description properties in DemoWidget as follows:


struct DemoWidget: Widget {
    let kind: String = "DemoWidget"

    var title: String = "My Widget"
    var desc: String = "This is an example widget."
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            DemoWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title) // Controls the display of Title in the Widget preview
        .description(desc) // Controls the display of Desc in the Widget preview
        .supportedFamilies([WidgetFamily.systemMedium])
    }
}

Then modify the places where DemoWidget is referenced, that is, in the WidgetBundleDemo class, passing different titles and descriptions as follows:


import SwiftUI
import WidgetKit

@main
struct WidgetBundleDemo: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        DemoWidget(title: "Sync Assistant", desc: "This is the QQ Sync Assistant Widget")
        DemoWidget(title: "Alipay", desc: "This is the Alipay Widget")
    }
}

Run again to check the effect, and you will find that the title and description also moved, as shown below:

image

Isn't it simple? The usage of WidgetBundle is as shown above. However, it should be noted that what is placed in WidgetBundle are all Widgets, and each Widget has its own Entry and Provider, meaning that each Widget in WidgetBundle needs to implement methods and content similar to DemoWidget.

Creating an Alipay Widget Component#

Next, let's implement the effect of the Alipay small component as shown below:

image image image image

UI Implementation#

Starting from the first image, let's break down the structure into two views: the left view is for the calendar and weather, while the right view contains four functional entry points. The overall size is medium. Let's implement it:

The code for the left view is as follows:

Now let's look at the right side with four functional entry points. Before creating the entry points, let's consider what fields this item should have. It needs to display an image and a title, and it should have a link to redirect when clicked. Additionally, SwiftUI's forEach traversal requires an id.

Next, looking at the Alipay widget, long press -> Edit Widget -> Select Function, we can see all selectable functions. Therefore, we need to define a type to enumerate all functions. Here, we will use eight as an example. The resource files are placed in the AlipayWidgetImages folder.

Thus, the overall definition of a single item corresponding to the functional entry is as follows:


import Foundation

public enum ButtonType: String {
    case Scan = "Scan"
    case pay = "Payment"
    case healthCode = "Health Code"
    case travelCode = "Travel Card"
    case trip = "Travel"
    case stuck = "Card Package"
    case memberpoints = "Membership Points"
    case yuebao = "Yu'ebao"
}

extension ButtonType: Identifiable {
    public var id: String {
        return rawValue
    }
    
    public var displayName: String {
        return rawValue
    }
    
    public var urlStr: String {
        let imageUrl: (image: String, url: String) = imageAndUrl(from: self)
        return imageUrl.url
    }
    
    public var imageName: String {
        let imageUrl: (image: String, url: String) = imageAndUrl(from: self)
        return imageUrl.image
    }
    
    /// return (image, url)
    func imageAndUrl(from type: ButtonType) -> (String, String) {
        switch self {
        case .Scan:
            return ("widget_scan", "https://www.baidu.com/")
        case .pay:
            return ("widget_pay", "https://www.baidu.com/")
        case .healthCode:
            return ("widget_healthCode", "https://www.baidu.com/")
        case .travelCode:
            return ("widget_travelCode", "https://www.baidu.com/")
        case .trip:
            return ("widget_trip", "https://www.baidu.com/")
        case .stuck:
            return ("widget_stuck", "https://www.baidu.com/")
        case .memberpoints:
            return ("widget_memberpoints", "https://www.baidu.com/")
        case .yuebao:
            return ("widget_yuebao", "https://www.baidu.com/")
        }
    }
}

struct AlipayWidgetButtonItem {
    var title: String
    var imageName: String
    var urlStr: String
    var id: String {
        return title
    }
    
    static func generateWidgetBtnItem(from originalItem: AlipayWidgetButtonItem) -> AlipayWidgetButtonItem {
        let newItem = AlipayWidgetButtonItem(title: originalItem.title,
                                             imageName: originalItem.imageName,
                                             urlStr: originalItem.urlStr)
        return newItem
    }
}

Now let's look at the implementation of the button group on the right side. Create AlipayWidgetGroupButtons.swift to encapsulate and display the four buttons, with the following code:


import SwiftUI

struct AlipayWidgetGroupButtons: View {
    var buttonList: [[AlipayWidgetButtonItem]]
    
    var body: some View {
        VStack() {
            ForEach(0..<buttonList.count, id: \.self) { index in
                HStack {
                    ForEach(buttonList[index], id: \.id) { buttonItem in
                        AlipayWidgetButton(buttonItem: buttonItem)
                    }
                }
            }
        }
    }
}

Next, create the left view, which is divided into three parts: weather, date, and a hint bar, with the hint bar encapsulated separately. The code is as follows:

Hint bar view:


import SwiftUI

struct AlipayWidgetLunarView: View {
    var body: some View {
        ZStack(alignment: .leading) {
            
            ZStack {
                AliPayLunarSubview()
                    .hidden()
            }
            .background(.white)
            .opacity(0.27)
            .cornerRadius(2.0)
            
            AliPayLunarSubview()
        }
    }
}

struct AliPayLunarSubview: View {
    var body: some View {
        HStack {
            Image("alipay")
                .resizable()
                .frame(width: 16.0, height: 16.0)
                .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))

            Text("Alipay")
                .font(Font.custom("Montserrat-Bold", size: 13.0))
                .minimumScaleFactor(0.5)
                .foregroundColor(.white)
                .padding(EdgeInsets(top: 4.0, leading: -7.0, bottom: 4.0, trailing: 0.0))

            Text("Today's Favorable")
                .font(Font.system(size: 10.0))
                .minimumScaleFactor(0.5)
                .foregroundColor(.white)
                .padding(EdgeInsets(top: 0.0, leading: -5.0, bottom: 0.0, trailing: 0.0))

            Image("right_Arrow")
                .resizable()
                .frame(width: 10, height: 10)
                .padding(EdgeInsets(top: 0.0, leading: -7.0, bottom: 0.0, trailing: 5.0))
        }
    }
}

The overall left view:


import SwiftUI

struct AlipayWidgetWeatherDateView: View {    
    var body: some View {
        VStack(alignment: .leading) {
            Spacer()

            Text("Cloudy 28℃")
                .font(.title)
                .foregroundColor(.white)
                .fontWeight(.semibold)
                .minimumScaleFactor(0.5)
                .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 4.0, trailing: 0.0))

            Text("06/09 Thursday Shanghai")
                .lineLimit(1)
                .font(.body)
                .foregroundColor(.white)
                .minimumScaleFactor(0.5)
                .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 4.0, trailing: 0.0))

            AlipayWidgetLunarView()

            Spacer()
        }
    }
}

Finally, combine the left view and the right button group, with the following code:


struct AlipayWidgetMeidumView: View {
    @ObservedObject var mediumItem: AliPayWidgetMediumItem

    var body: some View {
        ZStack {
            // Background image
            Image("widget_background_test")
                .resizable()
            HStack {
                AlipayWidgetWeatherDateView()
                
                Spacer()
                
                AlipayWidgetGroupButtons(buttonList: mediumItem.dataButtonList())
            }
            .padding()
        }
    }
}

The defined AliPayWidgetMediumItem is similar to a VM, converting the model into the data output needed for the view, with the following code:



class AliPayWidgetMediumItem: ObservableObject {
    @Published private var groupButtons: [[AlipayWidgetButtonItem]] = [[]]
    
    init() {
        self.groupButtons = AliPayWidgetMediumItem.createMeidumWidgetGroupButtons()
    }
    
    init(with widgetGroupButtons: [AlipayWidgetButtonItem]?) {
        guard let items = widgetGroupButtons else {
            self.groupButtons = AliPayWidgetMediumItem.createMeidumWidgetGroupButtons()
            return
        }
        
        var list: [[AlipayWidgetButtonItem]] = [[]]
        var rowList: [AlipayWidgetButtonItem] = []
        for i in 0..<items.count {
            let originalItem = items[i]
            let newItem = AlipayWidgetButtonItem.generateWidgetBtnItem(from: originalItem)
            if i != 0 && i % 2 == 0 {
                list.append(rowList)
                rowList = []
            }
            rowList.append(newItem)
        }
        
        if rowList.count > 0 {
            list.append(rowList)
        }
        self.groupButtons = list
    }
    
    private static func createMeidumWidgetGroupButtons() -> [[AlipayWidgetButtonItem]] {
        let scanType = ButtonType.Scan
        let scanItem = AlipayWidgetButtonItem(title: scanType.rawValue,
                                              imageName: scanType.imageName,
                                              urlStr: scanType.urlStr)
        
        let payType = ButtonType.pay
        let payItem = AlipayWidgetButtonItem(title: payType.rawValue,
                                             imageName: payType.imageName,
                                             urlStr: payType.urlStr)
        
        let healthCodeType = ButtonType.healthCode
        let healthCodeItem = AlipayWidgetButtonItem(title: healthCodeType.rawValue,
                                                    imageName: healthCodeType.imageName,
                                                    urlStr: healthCodeType.urlStr)

        let travelCodeType = ButtonType.travelCode
        let travelCodeItem = AlipayWidgetButtonItem(title: travelCodeType.rawValue,
                                                    imageName: travelCodeType.imageName,
                                                    urlStr: travelCodeType.urlStr)
        return [[scanItem, payItem], [healthCodeItem, travelCodeItem]]
    }
    
    func dataButtonList() -> [[AlipayWidgetButtonItem]] {
        return groupButtons
    }
}

Next, create the entry and Provider, with the following code:



import WidgetKit
import SwiftUI

struct AlipayWidgetProvider: TimelineProvider {
    typealias Entry = AlipayWidgetEntry
    
    func placeholder(in context: Context) -> AlipayWidgetEntry {
        AlipayWidgetEntry(date: Date())
    }
    
    func getSnapshot(in context: Context, completion: @escaping (AlipayWidgetEntry) -> Void) {
        let entry = AlipayWidgetEntry(date: Date())
        completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<AlipayWidgetEntry>) -> Void) {
        let entry = AlipayWidgetEntry(date: Date())
        // refresh the data every two hours
        let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
        let timeline = Timeline(entries: [entry], policy: .after(expireDate))
        completion(timeline)
    }
}

struct AlipayWidgetEntry: TimelineEntry {
    let date: Date
    
    
}

struct AlipayWidgetEntryView: View {
    var entry: AlipayWidgetProvider.Entry
    let mediumItem = AliPayWidgetMediumItem()
    
    var body: some View {
        AlipayWidgetMeidumView(mediumItem: mediumItem)
    }
}

struct AlipayWidget: Widget {
    let kind: String = "AlipayWidget"
    
    var title: String = "Alipay Widget"
    var desc: String = "Description of Alipay Widget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: AlipayWidgetProvider()) { entry in
            AlipayWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium])
    }
}

Finally, use it in the WidgetBundle as follows:


import SwiftUI
import WidgetKit

@main
struct WidgetBundleDemo: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        AlipayWidget(title: "Alipay", desc: "This is the Alipay Widget")
    }
}

The final display effect is as follows:

image

Using Widget Intent#

Static Intent Configuration#

Continuing from the above, comparing the Alipay widget, we can see that the Alipay widget shows an Edit Widget entry when long-pressed, while the one implemented above does not. Let's see how to implement this display.

The appearance of the Edit Widget entry requires creating an Intent. Then CMD+N to create a new one, search for intent, as shown in the image, and click next.

image

Then enter the name. Note that both the main target and the widget target must be checked. Click Create.

Open the newly created WidgetIntents, which is currently blank. Click the + in the lower left corner, as shown in the image.

image

You can see that there are four buttons to choose from: New Intent, Customize System Intent, New Enum, and New Type. Here, select New Intent.

Ps: Among the several entries, Customize System Intent is not commonly used, New Intent is almost mandatory; New Enum creates a new enumeration, and the name of this enumeration cannot be the same as the enumeration name in the code, so it needs to be converted when used; New Type creates a new class, which will be demonstrated later.

After clicking New Intent, pay attention to several aspects:

  • The name of the Intent needs to be modified because it defaults to Intent, and there may be more than one Intent file in the project, so the naming needs to be changed. When modifying the name, note that when used in the project, Intent will automatically be added to the modified name. For example, if modified to XXX, the name used in the project will be XXXIntent, so be careful not to repeat.
  • Then the Category of the Intent should be changed to View. The other types can be tried one by one if interested. The title below should also be changed to the name of the file.
  • Then, regarding the selections below, by default, Configurable in Shortcuts and Suggestions are checked. Here, uncheck these two and check Widgets, which is easy to understand. The more options checked, the more settings are required, so at the beginning, just checking Widgets is enough. After becoming familiar, if you want to set Siri Suggestions or Shortcuts, you can check the other two later.
image

Then click the + in the lower left corner again to add a new Enum. Note that the class name of the Enum cannot be the same as the name of the Enum in the project. The Enum is used for selection when clicking to edit the widget, so the contents of the Enum are defined according to the actual situation. The display name for adding cases can be in Chinese, which is consistent with the content of ButtonType in the project, as shown in the image below.

image

After adding the Enum, click on the previously created StaticConfiguration, and in the Parameter section, click to add a new one named btnType, changing the Type to the created Enum type, and unchecking Resolvable, as shown below:

image

At this point, the Intent has been added. Run and check the effect. If the Edit Widget entry still does not appear, why?

Although the Intent was created, the widget using the Intent has not been added. Therefore, we need to add a new widget that uses the Intent. The steps are as follows:

Create a StaticIntentWidgetProvider class, with the following code:


import Foundation
import WidgetKit
import SwiftUI

struct StaticIntentWidgetProvider: IntentTimelineProvider {
    
    typealias Entry = StaticIntentWidgetEntry
    typealias Intent = StaticConfigurationIntent
    
    // Convert the button type defined in the Intent to the button type used in the Widget
    func buttonType(from configuration: Intent) -> ButtonType {
        switch configuration.btnType {
        case .scan:
            return .scan
        case .pay:
            return .pay
        case .healthCode:
            return .healthCode
        case .travelCode:
            return .travelCode
        case .trip:
            return .trip
        case .stuck:
            return .stuck
        case .memberpoints:
            return .memberpoints
        case .yuebao:
            return .yuebao
        case .unknown:
            return .unknown
        }
    }
    
    func placeholder(in context: Context) -> StaticIntentWidgetEntry {
        StaticIntentWidgetEntry(date: Date())
    }
    
    func getSnapshot(for configuration: StaticConfigurationIntent, in context: Context, completion: @escaping (StaticIntentWidgetEntry) -> Void) {
        let buttonType = buttonType(from: configuration)
        
        let entry = StaticIntentWidgetEntry(date: Date())
        completion(entry)
    }
    
    func getTimeline(for configuration: StaticConfigurationIntent, in context: Context, completion: @escaping (Timeline<StaticIntentWidgetEntry>) -> Void) {
        let entry = StaticIntentWidgetEntry(date: Date())
        // refresh the data every two hours
        let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()
        let timeline = Timeline(entries: [entry], policy: .after(expireDate))
        completion(timeline)
    }
}

struct StaticIntentWidgetEntry: TimelineEntry {
    let date: Date
    
}

struct StaticIntentWidgetEntryView: View {
    var entry: StaticIntentWidgetProvider.Entry
    let mediumItem = AliPayWidgetMediumItem()
    
    var body: some View {
        AlipayWidgetMeidumView(mediumItem: mediumItem)
    }
}

struct StaticIntentWidget: Widget {

    let kind: String = "StaticIntentWidget"
    
    var title: String = "StaticIntentWidget"
    var desc: String = "Description of StaticIntentWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind,
                            intent: StaticConfigurationIntent.self,
                            provider: StaticIntentWidgetProvider()) { entry in
            StaticIntentWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium])
    }
}

Add it to the WidgetBundle as follows:


import SwiftUI
import WidgetKit

@main
struct WidgetBundleDemo: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        StaticIntentWidget()
    }
}

Run and check the effect as follows:

image image image

Note:

If after running, the Edit Widget appears, but the editing interface is empty and does not display the images from steps two and three above, you can check whether the Intent is checked for the main project, as shown below:

image

Dynamic Intent Configuration#

Continuing to compare with the Alipay widget, we can see that the Static Intent Configuration style implemented above is different from Alipay's, as Alipay displays multiple options, and the style for selecting each one is also different from the one implemented above. So how is this achieved?

The answer is Dynamic Intent Configuration. Let's continue:

Select the Intent and click to add New Intent, name it DynamicConfiguration, change the Category to View, check Widgets, and uncheck Configurable in Shortcuts and Suggestions, as shown:

image

Next, click to add New Type, name it CustomButtonItem, which will be used when adding Parameter in DynamicConfiguration. In Properties, add urlStr and imageName as String types, and add buttonType as the defined Enum type — ConfigrationButtonType, as shown:

image

Then, add Parameter to DynamicConfiguration, selecting Type as CustomButtonItem, checking Supports multiple values, Fixed Size, and Dynamic Options, unchecking Resolvable, and entering the prompt label Please select in Dynamic Options. The size in Fixed Size can be modified under different styles, as shown:

image

At this point, the settings in the Intent are complete. However, there is still a question: although Supports multiple values is checked in the Intent, where does the data come from? Where do the default displayed data come from when clicking to edit the widget? Where does all the data come from when clicking a single button?

The answer is Intent Extension. Click File -> New -> Target. Note that this is a Target, search for Intent, and select Intent Extension, as shown, then click next, uncheck Includes UI Extension, and click finish, as shown:

image image

Next, select the .intentdefinition file and check the newly created Target in Target Membership, as shown:

image

Then, select the project, select the WidgetIntentExtension Target, change the Deployment Info to 15.0, and in Supported Intents, click +, then enter DynamicConfigurationIntent, as shown:

image

Since the Intent Extension needs to use ButtonType from the Widget, select the class where ButtonType is located and check the Intent Extension Target in Target Membership, as shown:

Next, select IntentHandler, which is where the data source is located, and modify the content as follows:


import Intents

class IntentHandler: INExtension {
    
    override func handler(for intent: INIntent) -> Any {
        // This is the default implementation.  If you want different objects to handle different intents,
        // you can override this and return the handler you want for that particular intent.
        
        return self
    }
    
}

extension IntentHandler: DynamicConfigurationIntentHandling {
    func provideSelectButtonsOptionsCollection(for intent: DynamicConfigurationIntent, searchTerm: String?, with completion: @escaping (INObjectCollection<CustomButtonItem>?, Error?) -> Void) {
        let typeList: [ConfigrationButtonType] = [.scan, .pay, .healthCode, .trip, .travelCode, .stuck, .memberpoints, .yuebao]
        let itemList = generateItemList(from: typeList)
        completion(INObjectCollection(items: itemList), nil)
    }
    
    func defaultSelectButtons(for intent: DynamicConfigurationIntent) -> [CustomButtonItem]? {
        let defaultBtnTypeList: [ConfigrationButtonType] = [.scan, .pay, .healthCode, .trip]
        let defaultItemList = generateItemList(from: defaultBtnTypeList)
        return defaultItemList
    }
    
    fileprivate func generateItemList(from typeList: [ConfigrationButtonType]) -> [CustomButtonItem] {
        let defaultItemList = typeList.map({
            let formatBtnType = buttonType(from: $0)
            let item = CustomButtonItem(identifier: formatBtnType.id,
                                        display: formatBtnType.displayName)
            item.buttonType = $0
            item.urlStr = formatBtnType.urlStr
            item.imageName = formatBtnType.imageName
            return item
        })
        return defaultItemList
    }
    
    // Convert the button type defined in the Intent to the button type used in the Widget
    func buttonType(from configurationType: ConfigrationButtonType) -> ButtonType {
        switch configurationType {
        case .scan:
            return .scan
        case .pay:
            return .pay
        case .healthCode:
            return .healthCode
        case .travelCode:
            return .travelCode
        case .trip:
            return .trip
        case .stuck:
            return .stuck
        case .memberpoints:
            return .memberpoints
        case .yuebao:
            return .yuebao
        case .unknown:
            return .unknown
        }
    }
}

Finally, create a new IntentTimelineProvider to display this effect, with the following code:


import Foundation
import WidgetKit
import SwiftUI

struct DynamicIntentWidgetProvider: IntentTimelineProvider {
    typealias Entry = DynamicIntentWidgetEntry
    typealias Intent = DynamicConfigurationIntent

    func placeholder(in context: Context) -> DynamicIntentWidgetEntry {
        DynamicIntentWidgetEntry(date: Date())
    }
    
    func getSnapshot(for configuration: DynamicConfigurationIntent, in context: Context, completion: @escaping (DynamicIntentWidgetEntry) -> Void) {
        let entry = DynamicIntentWidgetEntry(date: Date(), groupBtns: configuration.selectButtons)
        completion(entry)
    }
    
    func getTimeline(for configuration: DynamicConfigurationIntent, in context: Context, completion: @escaping (Timeline<DynamicIntentWidgetEntry>) -> Void) {
        let entry = DynamicIntentWidgetEntry(date: Date(), groupBtns: configuration.selectButtons)
        let expireDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date()
        let timeline = Timeline(entries: [entry], policy: .after(expireDate))
        completion(timeline)
    }
}


struct DynamicIntentWidgetEntry: TimelineEntry {
    let date: Date
    var groupBtns: [CustomButtonItem]?
}

struct DynamicIntentWidgetEntryView: View {
    var entry: DynamicIntentWidgetProvider.Entry
    
    var body: some View {
        AlipayWidgetMeidumView(mediumItem: AliPayWidgetMediumItem(with: entry.groupBtns))
    }
}

struct DynamicIntentWidget: Widget {

    let kind: String = "DynamicIntentWidget"
    
    var title: String = "DynamicIntentWidget"
    var desc: String = "Description of DynamicIntentWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind,
                            intent: DynamicConfigurationIntent.self,
                            provider: DynamicIntentWidgetProvider()) { entry in
            DynamicIntentWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium])
    }
}

The effect is as follows:

image

At this point, we are almost done. Comparing with the Alipay widget, we can see that there are still styles for Displaying Weather and Selecting Function Position. In the DynamicConfiguration Parameter, directly add two properties: Selecting Function Position as Enum type and Displaying Weather as Bool type, then adjust the position, moving the selectButtons property to the bottom. The detailed steps can be tried by everyone.

The final effect is as follows:

image

Summary:#

Summary

The complete project code has been placed on github: https://github.com/mokong/WidgetAllInOne

Supplement:

If you want to refresh the widget, the default refresh timing is based on the timeline settings. However, if you want to force a refresh, for example, after an operation in the APP changes the state and you want the widget to refresh, you can use the following code, which can be called at the place where the refresh is triggered:


import WidgetKit

@objc
class WPSWidgetCenter: NSObject {
    @available(iOS 14, *)
    static func reloadTimelines(_ kind: String) {
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
    }
    
    @available(iOS 14, *)
    @objc static func reloadAllTimelines() {
        WidgetCenter.shared.reloadAllTimelines()
    }
}

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.