今是昨非

今是昨非

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

手把手教你創建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()
    }
}

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。