今是昨非

今是昨非

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

手把手教你创建widget2

手把手教你创建 widget2#

接上篇iOS Widget,这里介绍下 WidgetBundle 的用法和怎么做一个支付宝类似的 widget,上篇里把WidgetBundle写成了WidgetGroup,我的错。

WidgetBundle 的用法#

再来回顾一下什么情况下使用 WidgetBundle,上篇里介绍了supportedFamilies,可以设置 Widget 不同的尺寸,比如SmallMeidumLarge等,但是如果想要多个同尺寸的 Widget ,比如:想要两个Small尺寸的 Widget ,类似于下面东方财富 Widget 的效果,就需要用WidgetBundle,设置多个Widget

image image image

WidgetBundle的使用不难,下面来看下,上篇最后的代码(可以去https://github.com/mokong/WidgetAllInOne 下载,打开 Tutorial2),只显示了一个 Medium 尺寸的 Widget,这里修改为使用WidgetBundle显示两个Medium尺寸的 Widget。

新建 SwiftUIView,命名为WidgetBundleDemo,步骤如下:

  • 导入 WidgetKit
  • 修改 main 入口为 WidgetBundleDemo
  • 修改 WidgetBundleDemo 类型为 WidgetBundle
  • 修改 body 类型为 Widget

代码如下:


import SwiftUI
import WidgetKit

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

然后编译运行,failed,报错是xxx... error: 'main' attribute can only apply to one type in a module,意思是,一个 module 中只有有一个 @main,标记程序入口,所以需要移除多余的 @main,那哪里有呢,在 DemoWidget.swift 中,因为之前 main 入口是 DemoWidget,而现在的 main 入口是上面新建的 WidgetBundleDemo,所以需要移除 DemoWidget 中的 @main,移除后再次运行查看效果,发现添加 Widget 的预览中出现两个一模一样的 Medium 尺寸的 Widget。

Wait,上篇里说过,不同的 Widget 左右滑动的时候,上面的 title 和 desc 也是会跟着滑,为什么这里没有跟着滑?

确实是,嗯,应该是标题和内容一样的原因,一起来验证下,首先在 DemoWidget 中添加 title 和 desc 的属性,如下:


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) // 控制Widget预览中Title的显示
        .description(desc) // 控制Widget预览中Desc的显示
        .supportedFamilies([WidgetFamily.systemMedium])
    }
}

然后修改引用 DemoWidget 的地方,即 WidgetBundleDemo 类中,传入不同的标题和描述,如下:


import SwiftUI
import WidgetKit

@main
struct WidgetBundleDemo: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        DemoWidget(title: "同步助手", desc: "这是QQ同步助手Widget")
        DemoWidget(title: "支付宝", desc: "这是支付宝Widget")
    }
}

再次运行,查看效果,就会发现 title 和 desc 也移动了,效果如下:

image

很简单是不是,WidgetBundle的使用就是上面的用法,但是这里需要说明一点,WidgetBundle中放的都是Widget,而每个Widget都有自己EntryProvider,即:WidgetBundle中的每个Widget都需要实现类似DemoWidget的方法和内容。

创建一个支付宝 Widget 的组件#

然后来实现如下支付宝小组件的效果:

image image image image

UI 实现#

从第一张图开始,先来拆分结构,分为左右两个 view,左边 view 是日历 + 天气,右边是 4 个功能入口,整体是一个 medium 尺寸的,然后来实现:

左边的 view 代码如下:

再来看右侧 4 个功能入口,再创建入口之前,先来考虑一下创建入口对应的 Item,这个 Item 要有哪些字段?显示需要图片和标题,点击后跳转需要链接,另外 SwiftUI 中 forEach 遍历需要 id。

然后再看下支付宝 widget,长按 -> 编辑小组件 -> 选择功能,能看到所有可选的功能,所以这里需要定义一个 type,用于枚举所有的功能,这里仅以 8 个来示例。资源文件放在AlipayWidgetImages文件夹下。

所以功能入口对应的单个 item 整体定义如下:


import Foundation

public enum ButtonType: String {
    case Scan = "扫一扫"
    case pay = "收付款"
    case healthCode = "健康码"
    case travelCode = "行程卡"
    case trip = "出行"
    case stuck = "卡包"
    case memberpoints = "会员积分"
    case yuebao = "余额宝"
}

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
    }
}

然后来看右半边按钮组的实现,创建AlipayWidgetGroupButtons.swift,用于封装展示 4 个按钮的 view,代码如下:


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)
                    }
                }
            }
        }
    }
}

然后创建左半边的 view,分为三个部分,天气、日期、和提示条,其中提示条单独封装。代码如下:

提示条 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("支付宝")
                .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("今日宜")
                .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))
        }
    }
}

左半边 view 整体:


import SwiftUI

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

            Text("多云 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 周四 上海市")
                .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()
        }
    }
}

最后把左半边 view 和右半边的按钮组结合起来,代码如下:


struct AlipayWidgetMeidumView: View {
    @ObservedObject var mediumItem: AliPayWidgetMediumItem

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

其中定义的 AliPayWidgetMediumItem,是类似于 VM,将 model 转为 view 需要的数据输出,代码如下:



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
    }
}

然后创建入口和 Provider,代码如下:



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 = "支付宝Widget"
    var desc: String = "支付宝Widget描述"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: AlipayWidgetProvider()) { entry in
            AlipayWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium])
    }
}

最后在 WidgetBundle 中使用,如下:


import SwiftUI
import WidgetKit

@main
struct WidgetBundleDemo: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        AlipayWidget(title: "支付宝", desc: "这是支付宝Widget")
    }
}

最终显示效果如下:

image

Widget Intent 的使用#

Static Intent Configuration#

接着上面的来看,对比支付宝 widget,可以看到支付宝 widget 长按后会出现编辑小组件的入口,而上面实现的没有,下面就来看下如何实现这个的显示。

编辑小组件入口的出现,需要创建 Intent,然后CMD+N新建,搜索intent,如下图,点击下一步

image

然后输入名字,需注意的是这里的 target 要主Target和Widget Target都要勾选,点击Create

打开新建的 WidgetIntents,里面目前是空白,点击左下角的+,如下图

image

可以看到,有 4 个按钮可供选择,分别是New IntentCustomize System IntentNew EnumNew Type。这里选择New Intent

Ps: 几个入口中Customize System Intent不常用,New Intent几乎是必须要添加的;New Enum是新建一个枚举,这个枚举和代码中的枚举名字不能相同,所以使用时需要转换;New Type新建一个类,后面会有示范。

点击New Intent后,需要注意几个方面:

  • Intent 的名字需要修改,因为默认为Intent,而项目中可能有不止一个Intent文件,所以需要修改命名,修改命名时要注意的是在项目中使用时,会自动在修改的名字后面添加Intent,比如修改为XXX,项目中使用时的名字是XXXIntent,所以要注意不要重复
  • 然后是 Intent 的Category,这里修改为View,其他几个类型,感兴趣的可以一一尝试,下面的title也修改为文件的名字
  • 再然后是下面内容的勾选,默认勾选了Configurable in ShortcutsSuggestions,这里取消勾选这两个,改为勾选Widgets,意义很好理解。勾选的越多要设置的就越多,所以刚开始只需要勾选Widgets就够了,后面熟悉了,想要设置Siri建议或者快捷指令,再来勾选另外两个,尝试设置。
image

然后再来点击左下角的+,新增一个Enum,要注意的是 Enum 的类名不能和项目中 Enum 的名字一样,Enum 是用来选择,点击编辑小组件后进行选择的,所以 Enum 中的内容是根据实际来定义的,添加 case 的 displayName 可以为中文,在这里就是和项目中ButtonType的内容一致,如下图。

image

Enum 新增好了之后,再点击刚刚创建的StaticConfiguration,在 Parameter 部分点击新增,然后命名为btnType,修改 Type 为创建的 Enum 类型,取消勾选Resolvable,如下:

image

至此,Intent 添加完成,运行,查看效果,发现,依旧没有编辑小组件入口,为啥呢?

虽然创建了 Intent,但是并没有使用 Intent 的小组件,所以需要新增一个使用 Intent 的小组件,步骤如下:

新建StaticIntentWidgetProvider类,其中代码如下:


import Foundation
import WidgetKit
import SwiftUI

struct StaticIntentWidgetProvider: IntentTimelineProvider {
    
    typealias Entry = StaticIntentWidgetEntry
    typealias Intent = StaticConfigurationIntent
    
    // 将Intent中定义的按钮类型转为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 = "StaticIntentWidget描述"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind,
                            intent: StaticConfigurationIntent.self,
                            provider: StaticIntentWidgetProvider()) { entry in
            StaticIntentWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium])
    }
}

WidgetBundle中添加显示,如下:


import SwiftUI
import WidgetKit

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

运行查看效果如下:

image image image

备注:

如果运行后,出现了编辑小组件,但是点击后,编辑界面为空,没有显示上面步骤二和三的图片,可以查看 Intent 是否勾选到主项目,如下:

image

Dynamic Intent Configuration#

继续对比支付宝的 Widget,可以看到上面的实现的Static Intent Configuration样式和支付宝的并不相同,支付宝的展示了多个,且每个点击选择的样式也和上面实现的样式不同,所以是怎么实现的呢?

答案是Dynamic Intent Configuration,接着往下看:

选中 Intent,点击添加New Intent,命名为DynamicConfiguration,修改 Category 为View,勾选Widgets,取消勾选Configurable in ShortcutsSuggestions,如下:

image

继续,点击添加New Type,命名为CustomButtonItem,用于DynamicConfiguration中添加Parameter时使用。在Properties中添加urlStrimageName属性为 String 类型,再添加buttonType属性是定义的 Enum 类型 ——ConfigrationButtonType,如下:

image

然后,为DynamicConfiguration添加Parameter,选择 Type 为CustomButtonItem,勾选Supports multiple valuesFixed SizeDynamic Options,取消勾选Resolvable,在Dynamic Options下的Prompt Label中输入文案请选择,Fixed Size` 中不同样式下的 Size 可修改。如下:

image

到这里,Intent 中的设置已经完成了,但是还有个问题,虽然 Intent 中勾选了Supports multiple values,数据从哪里来,点击编辑小组件后,默认展示的几个数据是哪里来的?点击单个按钮时,跳转后展示的所有的数据是哪里来的?

答案是Intent Extension,点击 File -> New -> Target,这里注意,这个是Target,搜索Intent,选择Intent Extension,如下,点击下一步,取消勾选Includes UI Extension,点击完成,如下:

image image

然后,选中.intentdefinition文件,Target MemberShip中把刚刚创建的 Target 也勾选上,如下图:

image

再然后,选中项目,选中WidgetIntentExtensionTarget,修改Deployment Info15.0,在Supported Intents中点击+,然后输入DynamicConfigurationIntent,如下:

image

由于Intent Extension中要使用 Widget 中的ButtonType,所以选中ButtonType所在的类,在Target MemberShip中勾选Intent Extension的 Target,如下:

然后选中IntentHandler,这里面就是数据来源的地方,修改内容如下:


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
    }
    
    // 将Intent中定义的按钮类型转为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
        }
    }
}

最后,创建新的IntentTimelineProvider,来显示这个效果,代码如下:


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 = "DynamicIntentWidget描述"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind,
                            intent: DynamicConfigurationIntent.self,
                            provider: DynamicIntentWidgetProvider()) { entry in
            DynamicIntentWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(title)
        .description(desc)
        .supportedFamilies([.systemMedium])
    }
}

效果如下:

image

到此差不多就完成了,对比支付宝 widget,可以看到,还有展示天气选择功能位置的样式,在DynamicConfigurationParameter中,直接添加两个属性,选择功能位置Enum类型,展示天气Bool类型,然后调整位置,把selectButtons属性移到最下方,详细步骤大家自己尝试一下。

最终效果如下:

image

总结:#

总结

完整项目代码已放在github: https://github.com/mokong/WidgetAllInOne

补充:

如果想要刷新 widget,widget 默认刷新时机是根据 timiline 设置来的,但是如果想要强制刷新,比如在 APP 中操作了,状态发生了改变,想要 widget 里吗刷新,可以用如下代码,在触发刷新的地方调用即可:


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()
    }
}

参考#

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