迈着轻盈的脚步,Widgets它来了,无论Apple如何曾经如何不屑Android的交互设计,最终还是逃不过真香定律,也从侧面反映Apple现在确实创新乏力,否则不至于搬来Android的桌面小组件,可能有不少iPhone信仰粉失望了吧。
Widgets 支持在 iOS、iPadOS 主屏幕,今日视图以及 macOS 通知栏展示动态信息和个性化内容。 对于一向克制的Apple而言,引入 Widgets
无疑是一次巨大的改动。
对于使用过Android的同学而言,Widgets
非常好理解,其实就是把Android的桌面组件搬到iOS,并进行了一些符合Apple审美和要求的改造,从而诞生了Widgets
。
Widgets are not mini-apps
上面这句话在视频中被反复提起:不要认为Widgets
是一个放在桌面的微程序。
在一闪而过的快速切换应用的主屏幕里,设计交互复杂的应用界面并不能切合用户的需要,一目了然的内容才是用户关心的唯一要素。(Apple不仅仅口头说说而已,整个WidgetKit
都贯穿了这一点🧐)
通过强大的 SwiftUI
,你可以快速实现跨平台体验一致的 Widgets
,同时非常方便的支持 Dynamic Type
、DarkMode
等系统特性,提供一目了然、快速响应的桌面体验。
Glanceable(一目了然)
不要弄一些花里胡哨的页面试图塞满用户的眼球,提供的功能应该是目的明确,简洁清晰的,易于让用户一眼就知道这是展示的什么信息。
Relevant(有意义的)
不要显示枯燥的加载页面,应该在用户打开屏幕的第一时间,就展示给用户有意义的信息。因此,WidgetKit
统一管理Widgets
,支持预渲染、复用,并提供合适的更新策略,灵活可控的更新时机。
Personalized(个性化)
基于不同的外部因素(如环境,时间,温度等),提供个性化的使用体验。同时支持三种不同尺寸,越小就越需要展示更核心的信息,满足不同用户的需要。
多个 Widget
支持叠放组成Smart Stack
。用户可以上下滑动手动切换,不过更妙的是:Apple会基于用户的使用场景和习惯以及开发者的配置,智能地为用户展示最需要的 Widget
(不过还没来得及体验到Apple的智能切换🙃)。
系统显现Widget
的两个原因:
Intents framework
)与Keyboard Extension
,iMessage Extension
等一致,Apple新增一个Widget Extension
,那么同样具备App Group
数据共享等Extension
的能力。
是的,你没看错,只能用SwiftUI
开发,反观App Clip
并没有这个限制。不过Widgets
毕竟支持多端可用,那么使用为跨端而生的SwiftUI
也就能理解了。
如果对SwiftUI
不熟悉,可以翻看WWDC20 Session 10119: Introduction to SwiftUI。
SwiftUI
构建 Views
,定义 Timelines
为 Views
提供对应时间所需数据。数据变化时,通过 reload
更新数据。
其中Timelines
定义方法如下:
/// Provides an array of timeline entries for the current time and, /// optionally, any future times to update a widget. /// /// The `configuration` parameter provides user-customized values, as /// defined in your custom intent definition. /// /// - Parameters: /// - configuration: The intent containing user-customized values. /// - context: An object describing the context to show the widget in. /// - completion: The completion handler to call after you create the /// timeline. func timeline(for configuration: Self.Intent, with context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> ()) 复制代码
我们可以通过上述方法自由定义包含多种时间节点Entry
的时间线Timeline
,如下例子,就定义了一条从当前时间开始往后五个小时(间隔为一小时)的时间线:
public func timeline(for configuration: ConfigurationIntent, with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate, configuration: configuration) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } 复制代码
最终我们可以通过不同的Entry
来更新数据:
struct TestEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .time) } } 复制代码
类似Keyboard
,WidgetKit
统一管理多个 Widgets
:序列化 Widget Views
和 Timelines
数据,处理预渲染和复用,响应并处理 System reloads
、App-driven reloads
,为桌面、Widget Gallery
预览等场景提供高效、即时的 widget
体验。
WidgetKit
会把 Timelines
所定义的 Entries
对应的 Views
结构信息缓存到磁盘,仅在需要的时候实时渲染特定的快照 Snapshot
。这使得系统可以在极低电量开销下为众多 Widgets
处理 Timelines
信息。
为此Apple专门提供了一个输出快照Snapshot
的方法,我们可以直接使用当前时间对应的Entry
来立即渲染,当然也可以指定第一个Entry
(即初始化数据):
public func snapshot(for configuration: ConfigurationIntent, with context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration) completion(entry) } 复制代码
主要分为两种方式:System reloads
和App-driven reloads
。系统会综合判断,确定重新加载 Widget
的最佳时间。
System reloads:执行原理中已经提到过,也就是根据Timeline
刷新。
Widget
被查看越多,将更多的被 reload
。reload
(例如Snapshot
)。App-driven reloads:
reload
。reload
。但是要注意,App-driven
中的reload
只是能够变更全部或特定的 Entry
(相当于数据源),然后等待下一次Timeline
上的更新,并不是说可以直接刷新View
。
Apple支持small、medium、large三种尺寸,同时建议开发者同时提供三种尺寸(非必须),分别是 2 * 2、2 * 4 和 4 * 4 图标大小的规格。
对于不同尺寸下的布局形式,Apple也有示例:
虽然Apple不希望用户看到无意义的内容,但是有时候初始化时不可避免的需要占位图,对此Apple同样有非常明确的要求:应该清晰的表达 Widget
所属类型。
绝大多数场景下,用户不会看到 Placeholder UI ,只有在修改设备环境配置等个别场景下会碰到。
比较有意思的一点是,为了更加方便开发者使用,今年的 SwiftUI
还提供了一个特别棒的功能,可以通过 .isPlaceholder
修饰符来直接实现该效果,支持分别对单个视图的控制。
Widget
是无状态的,不支持滚动,不能展示视频和动态图像,不支持文字输入。如果用户试图点击Widget
,来进一步查看或编辑的时候,只能够跳转到App。
其实Widget
相当于信息展示+固定区域的Deep Link
点击跳转。
Deep Link
固定区域其实就是单个图标的区域,尺寸从小到大分别支持1,2,4个跳转入口。
Intents framework
一直被应用在 SiriKit
和 Shortcuts
,WidgetKit
也将借助 Intents framework
更好的理解用户意图。这个也就是影响Smart Stack
展示逻辑的策略之一。
开发者通过每一个 TimelineEntryRelevance
可以告知 WidgetKit
用户在特定场景下相关性评分和持续时间。WidgetKit
综合评估不同 Widget
评分,分析用户意图,旋转堆栈到用户关心的 Widget
。
蜗牛对这个框架不了解,时间有限也没有去深入尝试,感兴趣的同学可以自己研究下~
还有个有意思的小东西,由于不同大小的设备可能对其 Widget
使用不同的半径,而子视图往往也需要同步修改,这无疑是比较麻烦的。Appe新增的 ContainerRelativeShape
是一种新的形状类型,它将采用最接近父视图容器形状的路径,并根据形状的位置使用适当的角半径。
一开始以为Widget
能够自动适配Today
组件,但是更新了系统之后,发现只有系统自己的可以直接移到主屏幕,三方的Today
组件并不能适配。(不得不吐槽一句,Apple组件也并没有通过Widget
重写,实际上技术是满足的,只是Apple为了推广SwiftUI
的策略)
另外查看Today Extension
的相关API中,也明确提到了已过期,需要使用WidgetKit
:
由此可见,Today
组件已然被抛弃,随着iOS14的快速普及,开发的价值将会越来越小。
虽然看起来和Android类似,但是Widget
无疑充满了一贯的Apple风格,明确的条条框框将开发者定的死死的,交互非常弱,可能很难满足一些产品的需求。
Widget
的定位还是比较清晰的,用于弥补主App无法及时展示用户所关心的数据。
随着越来越强大的 SwiftUI
,Widget
能够在Apple生态中更如鱼得水,相信以后的功能也会越来越强大。
Widget
强制使用SwiftUI
,对于这样一个重要的组件来说,大部分开发者都需要学习SwiftUI
,这无疑会加速 iOS 和 macOS 生态的融合。
Meet WidgetKit
Build SwiftUI views for widgets
Widgets Code-along, part 1: The adventure begins
Widgets Code-along, part 2: Alternate timelines
Widgets Code-along, part 3: Advancing timelines
Design great widgets
Add configuration and intelligence to your widgets
原创不易,文章有任何错误,欢迎批(feng)评(kuang)指(diao)教(wo),顺手点个赞👍,不甚感激!