手把手教你创建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
代码如下:
1 |
|
然后编译运行,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的属性,如下:
1 |
|
然后修改引用DemoWidget的地方,即WidgetBundleDemo类中,传入不同的标题和描述,如下:
1 |
|
再次运行,查看效果,就会发现title和desc也移动了,效果如下:
很简单是不是,WidgetBundle的使用就是上面的用法,但是这里需要说明一点,WidgetBundle中放的都是Widget,而每个Widget都有自己Entry和Provider,即:WidgetBundle中的每个Widget都需要实现类似DemoWidget的方法和内容。
创建一个支付宝Widget的组件
然后来实现如下支付宝小组件的效果:
UI实现
从第一张图开始,先来拆分结构,分为左右两个view,左边view是日历+天气,右边是4个功能入口,整体是一个medium尺寸的,然后来实现:
左边的view代码如下:
1 |
再来看右侧4个功能入口,再创建入口之前,先来考虑一下创建入口对应的Item,这个Item要有哪些字段?显示需要图片和标题,点击后跳转需要链接,另外SwiftUI中forEach遍历需要id。
然后再看下支付宝widget,长按 -> 编辑小组件 -> 选择功能,能看到所有可选的功能,所以这里需要定义一个type,用于枚举所有的功能,这里仅以8个来示例。资源文件放在AlipayWidgetImages文件夹下。
所以功能入口对应的单个item整体定义如下:
1 |
|
然后来看右半边按钮组的实现,创建AlipayWidgetGroupButtons.swift,用于封装展示4个按钮的view,代码如下:
1 |
|
然后创建左半边的view,分为三个部分,天气、日期、和提示条,其中提示条单独封装。代码如下:
提示条view:
1 |
|
左半边view整体:
1 |
|
最后把左半边view和右半边的按钮组结合起来,代码如下:
1 |
|
其中定义的AliPayWidgetMediumItem,是类似于VM,将model转为view需要的数据输出,代码如下:
1 |
|
然后创建入口和Provider,代码如下:
1 |
|
最后在WidgetBundle中使用,如下:
1 |
|
最终显示效果如下:
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类,其中代码如下:
1 |
|
在WidgetBundle中添加显示,如下:
1 |
|
运行查看效果如下:
备注:
如果运行后,出现了编辑小组件,但是点击后,编辑界面为空,没有显示上面步骤二和三的图片,可以查看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也勾选上,如下图:
再然后,选中项目,选中WidgetIntentExtensionTarget,修改Deployment Info为15.0,在Supported Intents中点击+,然后输入DynamicConfigurationIntent,如下:
由于Intent Extension中要使用Widget中的ButtonType,所以选中ButtonType所在的类,在Target MemberShip中勾选Intent Extension的Target,如下:
然后选中IntentHandler,这里面就是数据来源的地方,修改内容如下:
1 |
|
最后,创建新的IntentTimelineProvider,来显示这个效果,代码如下:
1 |
|
效果如下:
到此差不多就完成了,对比支付宝widget,可以看到,还有展示天气和选择功能位置的样式,在DynamicConfiguration的Parameter中,直接添加两个属性,选择功能位置为Enum类型,展示天气为Bool类型,然后调整位置,把selectButtons属性移到最下方,详细步骤大家自己尝试一下。
最终效果如下:
总结:

完整项目代码已放在github: https://github.com/mokong/WidgetAllInOne
补充:
如果想要刷新widget,widget默认刷新时机是根据timiline设置来的,但是如果想要强制刷新,比如在APP中操作了,状态发生了改变,想要widget里吗刷新,可以用如下代码,在触发刷新的地方调用即可:
1 |
|