WWDC20 第三弹 - 姗姗来迟的Widgets

2,573 阅读8分钟
迈着轻盈的脚步,Widgets它来了,无论Apple如何曾经如何不屑Android的交互设计,最终还是逃不过真香定律,也从侧面反映Apple现在确实创新乏力,否则不至于搬来Android的桌面小组件,可能有不少iPhone信仰粉失望了吧。

什么是Widgets?


Widgets 支持在 iOS、iPadOS 主屏幕,今日视图以及 macOS 通知栏展示动态信息和个性化内容。 对于一向克制的Apple而言,引入 Widgets 无疑是一次巨大的改动。

对于使用过Android的同学而言,Widgets 非常好理解,其实就是把Android的桌面组件搬到iOS,并进行了一些符合Apple审美和要求的改造,从而诞生了Widgets

Widgets are not mini-apps

上面这句话在视频中被反复提起:不要认为Widgets是一个放在桌面的微程序

在一闪而过的快速切换应用的主屏幕里,设计交互复杂的应用界面并不能切合用户的需要,一目了然的内容才是用户关心的唯一要素。(Apple不仅仅口头说说而已,整个WidgetKit都贯穿了这一点🧐)

通过强大的 SwiftUI,你可以快速实现跨平台体验一致的 Widgets,同时非常方便的支持 Dynamic TypeDarkMode 等系统特性,提供一目了然、快速响应的桌面体验。

一个优秀的Widgets具备什么?

  • Glanceable(一目了然)

    不要弄一些花里胡哨的页面试图塞满用户的眼球,提供的功能应该是目的明确,简洁清晰的,易于让用户一眼就知道这是展示的什么信息。

  • Relevant(有意义的)

    不要显示枯燥的加载页面,应该在用户打开屏幕的第一时间,就展示给用户有意义的信息。因此,WidgetKit 统一管理Widgets,支持预渲染、复用,并提供合适的更新策略,灵活可控的更新时机。

  • Personalized(个性化)

    基于不同的外部因素(如环境,时间,温度等),提供个性化的使用体验。同时支持三种不同尺寸,越小就越需要展示更核心的信息,满足不同用户的需要。

Smart Stack

多个 Widget 支持叠放组成Smart Stack。用户可以上下滑动手动切换,不过更妙的是:Apple会基于用户的使用场景和习惯以及开发者的配置,智能地为用户展示最需要的 Widget(不过还没来得及体验到Apple的智能切换🙃)。

系统显现Widget的两个原因:

  1. 用户行为习惯
  2. App提供的相关信息(基于Intents framework

实现方式


新增的Extension

Keyboard ExtensioniMessage Extension等一致,Apple新增一个Widget Extension,那么同样具备App Group数据共享等Extension的能力。

仅支持SwiftUI

是的,你没看错,只能用SwiftUI开发,反观App Clip并没有这个限制。不过Widgets毕竟支持多端可用,那么使用为跨端而生的SwiftUI也就能理解了。

如果对SwiftUI不熟悉,可以翻看WWDC20 Session 10119: Introduction to SwiftUI

执行原理

开发者通过 SwiftUI 构建 Views,定义 TimelinesViews 提供对应时间所需数据。数据变化时,通过 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)
   }
}

系统统一管理

类似KeyboardWidgetKit 统一管理多个 Widgets:序列化 Widget ViewsTimelines 数据,处理预渲染和复用,响应并处理 System reloadsApp-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 reloadsApp-driven reloads。系统会综合判断,确定重新加载 Widget 的最佳时间。

System reloads:执行原理中已经提到过,也就是根据Timeline刷新。

  • Widget 被查看越多,将更多的被 reload
  • 设备环境变化也会触发 reload(例如Snapshot)。

App-driven reloads

  • app在后台时,后台推送可以触发 reload
  • app在前台时,应用可以主动触发 reload

但是要注意,App-driven中的reload只是能够变更全部或特定的 Entry(相当于数据源),然后等待下一次Timeline上的更新,并不是说可以直接刷新View

三种尺寸

Apple支持small、medium、large三种尺寸,同时建议开发者同时提供三种尺寸(非必须),分别是 2 * 2、2 * 4 和 4 * 4 图标大小的规格。

对于不同尺寸下的布局形式,Apple也有示例:

Placeholder UI

虽然Apple不希望用户看到无意义的内容,但是有时候初始化时不可避免的需要占位图,对此Apple同样有非常明确的要求:应该清晰的表达 Widget 所属类型。

绝大多数场景下,用户不会看到 Placeholder UI ,只有在修改设备环境配置等个别场景下会碰到。

比较有意思的一点是,为了更加方便开发者使用,今年的 SwiftUI 还提供了一个特别棒的功能,可以通过 .isPlaceholder 修饰符来直接实现该效果,支持分别对单个视图的控制。

弱交互

Widget是无状态的,不支持滚动,不能展示视频和动态图像,不支持文字输入。如果用户试图点击Widget,来进一步查看或编辑的时候,只能够跳转到App。

其实Widget相当于信息展示+固定区域的Deep Link点击跳转。

Deep Link固定区域其实就是单个图标的区域,尺寸从小到大分别支持1,2,4个跳转入口。

Intents framework

Intents framework 一直被应用在 SiriKitShortcutsWidgetKit 也将借助 Intents framework 更好的理解用户意图。这个也就是影响Smart Stack展示逻辑的策略之一。

开发者通过每一个 TimelineEntryRelevance 可以告知 WidgetKit 用户在特定场景下相关性评分和持续时间。WidgetKit 综合评估不同 Widget 评分,分析用户意图,旋转堆栈到用户关心的 Widget

蜗牛对这个框架不了解,时间有限也没有去深入尝试,感兴趣的同学可以自己研究下~

ContainerRelativeShape

还有个有意思的小东西,由于不同大小的设备可能对其 Widget 使用不同的半径,而子视图往往也需要同步修改,这无疑是比较麻烦的。Appe新增的 ContainerRelativeShape 是一种新的形状类型,它将采用最接近父视图容器形状的路径,并根据形状的位置使用适当的角半径。

Today组件何去何从?


一开始以为Widget能够自动适配Today组件,但是更新了系统之后,发现只有系统自己的可以直接移到主屏幕,三方的Today组件并不能适配。(不得不吐槽一句,Apple组件也并没有通过Widget重写,实际上技术是满足的,只是Apple为了推广SwiftUI的策略)

另外查看Today Extension的相关API中,也明确提到了已过期,需要使用WidgetKit:

由此可见,Today组件已然被抛弃,随着iOS14的快速普及,开发的价值将会越来越小。

总结


  • 虽然看起来和Android类似,但是Widget无疑充满了一贯的Apple风格,明确的条条框框将开发者定的死死的,交互非常弱,可能很难满足一些产品的需求。

  • Widget 的定位还是比较清晰的,用于弥补主App无法及时展示用户所关心的数据。

  • 随着越来越强大的 SwiftUIWidget 能够在Apple生态中更如鱼得水,相信以后的功能也会越来越强大。

  • Widget强制使用SwiftUI,对于这样一个重要的组件来说,大部分开发者都需要学习SwiftUI,这无疑会加速 iOS 和 macOS 生态的融合。

相关Session

原创不易,文章有任何错误,欢迎批(feng)评(kuang)指(diao)教(wo),顺手点个赞👍,不甚感激!