实际上这并不是我首次在项目中使用到widget,先后在两个项目中涉及到widget,首次使用应该是16年的项目中。说来有些惭愧,虽然已经在两个项目中使用widget,但是除了业务逻辑,针对widget的逻辑(基本是UI逻辑)都只是简单的粘贴复制,并没有对其中的相关API进行仔细分析,所以当时为了调节和需求对应的效果,盲目的模仿一些文章调节参数。
当时碰到的比较棘手的问题算只有两个:
- iOS9 -> iOS10: 因为在iOS10之后,widget需要重新进行适配
- iOS10-> Now: 折叠/展开按钮的状态控制。
首先还是要提一下自己的一个关于widget生命周期的认知错误,挺长一段时间我都认为,widget在拉下来的时候并不会都执行viewDidLoad()
,而是有点类似UITabBarController'chilerViewContoller
,只创建一次,后面每次点击item都是重新appear
。基于这样的错误思维,实际开发中,基本没有在viewDidLoad()
中加入什么逻辑,而是把相应的逻辑都转移到了viewWillAppear()
中。
在iOS10之后,苹果引入新的API来管理控制widgetUI,起决定作用的是一个枚举(NCWidgetDisplayMode
)和对协议(NCWidgetProviding
)的补充。
NCWidgetDisplayMode
1 | @available(iOS 10.0, *) |
与其对应的是NSExtensionContext
下的两个扩展属性widgetLargestAvailableDisplayMode
、widgetActiveDisplayMode
widgetLargestAvailableDisplayMode(ios10)
先看下官方的部分使用解释:
Widgets can change the largest display mode they make available from the default ‘NCWidgetDisplayModeCompact’ by messaging the extension context.
Modifying this property more than once during the lifetime of the widget (perhaps due to changes in the amount of available content) is supported.
粗暴理解下,该属性用于控制widget的最大显示模式,并且可以根据显示内容的改变它的值。
默认
= .compact
,widget处于一种固定高度的状态110pt(并未在所有机型上验证),且无法修改,widget会隐藏展开/折叠按钮。于是就引出了上面提到的第一个问题,在<= iOS9
的时候,widget高度都是直接使用preferredContentSize
控制,但是用户升级到>= iOS10
之后,preferredContentSize
便无法控制它的高度,一直处于110pt状态,导致内容无法全部显示。当
= .expanded
,此时widget处于高度可变的状态,且会显示展开/折叠按钮,但是按钮当前的状态和它无关(这也是我曾经的一个误区)。注意一点:如果手动设置该属性,必须要在后面设置相应状态的
preferredContentSize
属性,否则会出现异常的结果,见遇到的坑所以可以根据需求进行相应的设置,如果高度固定,且只有110pt,那使用默认就好,否则只能设置
.expanded
。假设场景:最大会在widget中显示四行四列16个model的方格布局,在从服务器中拿到数据之后,如果不超过4个model,直接设置.compact
隐藏掉展开/折叠按钮,多于4个model的时候,设置.expanded
显示展开/折叠按钮。
widgetActiveDisplayMode(iOS10)
这是个read-only
属性,对应于widget所处于的状态和展开/折叠按钮的状态,= .compact
widget处于已折叠状态,按钮显示展开,= .expanded
widget处于已展开的状态,按钮显示折叠。从现象上看它的值(状态)是被手机单独记录下来的,并不是每次拉下来widget都是回到默认值(事实上它也没有默认值),而是保留了上次点击展开/折叠按钮所触发的状态改变
假设场景:项目中可以在请求到服务器model后,根据该属性的值来决定要显示多少个model,设置多大的preferredContentSize
,= .compact
显示部分,= .expanded
显示全部。
但是如果处理不当,widget的状态可能会和你设置的相应状态下的preferredContentSize
不匹配,见遇到的坑
NCWidgetProviding
先看一下该协议在针对>= iOS10
进行的修改:
废弃了下面的接口:
1 |
|
<iOS10
widget是默认有内容margin调节的。
补充了新的接口1
2
3
4// If implemented, called when the active display mode changes.
// The widget may wish to change its preferredContentSize to better accommodate the new display mode.
@available(iOS 10.0, *)
optional public func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize)
这就是点击展开/折叠按钮产生的回调,根据新的activeDisplayMode
设置preferredContentSize
遇到的坑
点击展开/折叠按钮触发回调
1
2
3
4
5
6if activeDisplayMode == .compact {
preferredContentSize = CGSize(width: viewWidth, height: 110)
} else {
setFullSize()
}
}我们已经知道在
= .compact
模式下,widget的高度是定死110pt的,但是在回调中第二行的代码不可少,缺少的话会产生一种情况:
从GIF中看出,即使没有第二行代码,第一次点击折叠的时候,widget正常折叠会110pt,但是接下来的点击展开/折叠,虽然activeDisplayMode
在切换,但是preferredContentSize
已经失效。和情况1相似,只不过触发点不同。假设场景:widget最多显示四行四列16个model,每四个model widget增加一个高度,不超过四个model的时候隐藏展开/折叠按钮,否则显示出来。从服务请求到model数据之后,设置
widgetLargestAvailableDisplayMode
控制按钮的显示,但是一定要在之后设置响应状态的preferredContentSize
,且最好保持这个先后顺序,开发中偶然没有按照这个先后顺序,出现了preferredContentSize
无效的情况。1-5和6-10顺序 1
2
3
4
5
6
7
8
9
10if dataSource.count <= 4 {
extensionContext?.widgetLargestAvailableDisplayMode = .compact
} else {
extensionContext?.widgetLargestAvailableDisplayMode = .expanded
}
if extensionContext?.widgetActiveDisplayMode == .compact {
preferredContentSize = CGSize(width: viewWidth, height: 110)
} else {
preferredContentSize = CGSize(width: viewWidth, height: 110 * rowCount)
}Widget也是一个单独的APP,但是无法进行debug,设置debugpoint,如果出现bug的话会显示无法加载,目前能想到的debug方式是添加
UIlabel
显示log。写widget的时候,看了很多的文章,但是基本上都是在
viewDidLoad()
中设置了extensionContext?.widgetLargestAvailableDisplayMode = .expanded
,其实是没有必要的,并不是必须这样的,还是要根据需求走,在合适的时机设置extensionContext?.widgetLargestAvailableDisplayMode
合适的值,控制展开/折叠按钮的显示隐藏状态。
在stackoverflow看到有人提出这样问题:在不显示展开/折叠按钮的情况下,给widget设置另外一个高度(非默认110pt)的,并且提供了一个已经实现这种逻辑的APP截图,我也非常好奇怎么实现的。