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