手把手教你创建 widget2#
接上篇iOS Widget,這裡介紹下 WidgetBundle 的用法和怎麼做一個支付寶類似的 widget,上篇裡把WidgetBundle
寫成了WidgetGroup
,我的錯。
WidgetBundle 的用法#
再來回顧一下什麼情況下使用 WidgetBundle
,上篇裡介紹了supportedFamilies
,可以設置 Widget 不同的尺寸,比如Small
、Meidum
、Large
等,但是如果想要多個同尺寸的 Widget ,比如:想要兩個Small
尺寸的 Widget ,類似於下面東方財富 Widget 的效果,就需要用WidgetBundle
,設置多個Widget
。
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 也移動了,效果如下:
很簡單是不是,WidgetBundle
的使用就是上面的用法,但是這裡需要說明一點,WidgetBundle
中放的都是Widget
,而每個Widget
都有自己Entry
和Provider
,即:WidgetBundle
中的每個Widget
都需要實現類似DemoWidget
的方法和內容。
創建一個支付寶 Widget 的組件#
然後來實現如下支付寶小組件的效果:
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")
}
}
最終顯示效果如下:
Widget Intent 的使用#
Static Intent Configuration#
接著上面的來看,對比支付寶 widget,可以看到支付寶 widget 長按後會出現編輯小組件
的入口,而上面實現的沒有,下面就來看下如何實現這個的顯示。
編輯小組件
入口的出現,需要創建 Intent,然後CMD+N
新建,搜索intent
,如下圖,點擊下一步
然後輸入名字,需注意的是這裡的 target 要主Target和Widget Target
都要勾選,點擊Create
打開新建的 WidgetIntents,裡面目前是空白,點擊左下角的+
,如下圖
可以看到,有 4 個按鈕可供選擇,分別是New Intent
、Customize System Intent
、New Enum
、New 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 Shortcuts
和Suggestions
,這裡取消勾選這兩個,改為勾選Widgets
,意義很好理解。勾選的越多要設置的就越多,所以剛開始只需要勾選Widgets
就夠了,後面熟悉了,想要設置Siri建議
或者快捷指令
,再來勾選另外兩個,嘗試設置。
然後再來點擊左下角的+
,新增一個Enum
,要注意的是 Enum 的類名不能和項目中 Enum 的名字一樣,Enum 是用來選擇,點擊編輯小組件後進行選擇的,所以 Enum 中的內容是根據實際來定義的,添加 case 的 displayName 可以為中文,在這裡就是和項目中ButtonType
的內容一致,如下圖。
Enum 新增好了之後,再點擊剛剛創建的StaticConfiguration
,在 Parameter 部分點擊新增,然後命名為btnType
,修改 Type 為創建的 Enum 類型,取消勾選Resolvable
,如下:
至此,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()
}
}
運行查看效果如下:
備註:
如果運行後,出現了編輯小組件
,但是點擊後,編輯界面為空,沒有顯示上面步驟二和三的圖片,可以查看 Intent 是否勾選到主項目,如下:
Dynamic Intent Configuration#
繼續對比支付寶的 Widget,可以看到上面的實現的Static Intent Configuration
樣式和支付寶的並不相同,支付寶的展示了多個,且每個點擊選擇的樣式也和上面實現的樣式不同,所以是怎麼實現的呢?
答案是Dynamic Intent Configuration
,接著往下看:
選中 Intent,點擊添加New Intent
,命名為DynamicConfiguration
,修改 Category 為View
,勾選Widgets
,取消勾選Configurable in Shortcuts
和Suggestions
,如下:
繼續,點擊添加New Type
,命名為CustomButtonItem
,用於DynamicConfiguration
中添加Parameter
時使用。在Properties
中添加urlStr
和imageName
屬性為 String 類型,再添加buttonType
屬性是定義的 Enum 類型 ——ConfigrationButtonType,如下:
然後,為DynamicConfiguration
添加Parameter
,選擇 Type 為CustomButtonItem
,勾選Supports multiple values
、Fixed Size
、Dynamic Options
,取消勾選Resolvable
,在Dynamic Options
下的Prompt Label
中輸入文案請選擇
,Fixed Size
中不同樣式下的 Size 可修改。如下:
到這裡,Intent 中的設置已經完成了,但是還有個問題,雖然 Intent 中勾選了Supports multiple values
,數據從哪裡來,點擊編輯小組件後,默認展示的幾個數據是從哪裡來的?點擊單個按鈕時,跳轉後展示的所有的數據是從哪裡來的?
答案是Intent Extension
,點擊 File -> New -> Target,這裡注意,這個是Target,搜索Intent
,選擇Intent Extension
,如下,點擊下一步,取消勾選Includes UI Extension
,點擊完成,如下:
然後,選中.intentdefinition
文件,Target MemberShip
中把剛剛創建的 Target 也勾選上,如下圖:
再然後,選中項目,選中WidgetIntentExtension
Target,修改Deployment Info
為15.0
,在Supported Intents
中點擊+
,然後輸入DynamicConfigurationIntent
,如下:
由於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])
}
}
效果如下:
到此差不多就完成了,對比支付寶 widget,可以看到,還有展示天氣
和選擇功能位置
的樣式,在DynamicConfiguration
的Parameter
中,直接添加兩個屬性,選擇功能位置
為Enum
類型,展示天氣
為Bool
類型,然後調整位置,把selectButtons
屬性移到最下方,詳細步驟大家自己嘗試一下。
最終效果如下:
總結:#
完整項目代碼已放在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()
}
}