iOS 14 Widget 上手体验

swift 发布于 2020年10月31日

今年的线上 WWDC 已经开始有一段时间了. 苹果在 iOS 14 中进行了一系列更新. 其中桌面 Widget 是最引人注意的一次革新. 在之前的很多年, 桌面 Widget 一直都存在于 Android 平台, 很多 Android 平台上利用 Widget 提供的体验, 如今在 iOS 14 中也成为可能了.

这篇文章给大家介绍如何创建一个最简单的桌面 Widget, 让大家能对 Widget 有一个快速的了解. 更深入的功能以后再和大家探讨.

创建 Widget 组件

要开发新的 Widget 组件, 首先你要准备 Xcode 12 Beta 版本, 以及一台安装了 iOS 14 Beta 版本的设备, 或者直接使用模拟器.

Widget 的基本创建并不复杂, 如果你没有时间亲自动手写代码, 看一篇这篇文章也足够了解了.

  • 在 Xcode 12 中创建一个新项目, 并且在项目设置页面里,点击左下角的箭头创建新的 Target:

  • 在弹出的组件选择窗口中, 在右上角的搜索栏中输入 widget, 然后在过滤后列表中选中 Widget Extension, 点击 Next 继续.

  • 接着, 输入 widget 的名称, 并且取消 Include Configuration Intent 的选择, 点击 Finish.

  • Widget 创建好之后, 会看到这几个文件:

  • 创建好后, Cmd + R 运行当前程序. 回到桌面后长按空白处, 弹出设置界面, 点击左上角的添加按钮:

  • 在列表中选中我们的 app:

  • 这样, 我们可以看到 Wdiget 的三种预览:

  • 选择一个预览, 就可以把 Widget 添加到桌面上:

  • 上图是我们创建的这个 Wdiget 的默认界面, 显示了一个当前时间的时钟.
了解 Widget 流程

上面我们介绍了 Widget 创建的基本流程. 接下来我们看一下如何自定义 Widget 界面.

首先, 根据目前的信息, Widget 只支持 SwiftUI 界面.

这里是 Widget 的主入口:

@main
struct widget: Widget {

private let kind: String = "widget"

public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
widgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}

我们把上面这段代码分成3个部分来解读:

private let kind: String = "widget"

这个定义的 kind 表示 Widget 的唯一标识.

StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
widgetEntryView(entry: entry)
}

StaticConfiguration 接受3个参数.

kind 是我们刚说的, 唯一标识.
provider 可以理解为 Widget 的数据提供者 placeholder 是在 Widget 数据实际加载完成之前, 显示的一个占位符. widgetEntryView(entry: entry) 是 Widget 实际的 VIew 层. 也就是UI界面.

.configurationDisplayName("My Widget")
.description("This is an example widget.")

这两行用来设置 WIdget 添加界面中, 显示的标题和描述, 如下图:

使用 StaticConfiguration 的 Widget 基本结构就是这样, kind 提供唯一标识, provider 提供数据支持, placeholder 提供占位符视图. 内部返回的 widgetEntryView 作为 UI 界面.

我们逐一分析下每一个组件.

  • 先从 widgetEntryView 开始:
struct widgetEntryView : View {
var entry: Provider.Entry

var body: some View {
Text(entry.date, style: .time)
}
}

这是一个典型的 SwiftUI 视图定义, body 属性里面包含了一个 Text 视图. 将 entry 中包含的日期作为文本显示给用户. 也就是我们看到的时钟 Widget 了.

  • 然后我们再看下 placeholder:
struct PlaceholderView : View {
var body: some View {
Text("Placeholder View")
}
}

这也是一个 SwiftUI 视图, 直接在 Text 视图上显示 "Placeholder View" 这个文本. 在我们的 Wdiget 刚刚添加到界面时候, 你有机会看到这个视图, 随后真实的数据加载完成, 它就会消失, 被真正的 Widget 视图所取代.

因为这个示例 Widget 使用的是当前时间, 这个数据的加载非常快, 很可能你不一定看得到这个 PlaceholderView. 如果你的 Widget 数据加载逻辑比较耗时间, 这个 PlaceholderView 的作用就体现出来了.

  • 再来到 Wdiget 数据的核心 provider:
struct Provider: TimelineProvider {
public typealias Entry = SimpleEntry

public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}

public func timeline(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)
entries.append(entry)
}

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}

这里使用的是 TimelineProvider 我们需要实现它的两个方法: snapshottimeline.

snapshot 可以理解为快照, 它会在 Widget 初始化的时候调用. 用于提供即时预览的数据. 比如 Widget 真实数据需要较长时间加载, 像是需要网络读取等. 那么系统会先调用 snapshot 来提供一个马上能用的数据, 这也就是说 snapshot 方法提供的数据, 应该尽量快速. 我们来看看代码:

public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
completion(entry)
}

这里用当前时间 Date(), 初始化了一个 SimpleEntry 实例. 然后传递进 completion(entry) 的调用. 这个方法你可以理解为将 entry 实体对象作为数据传递给 Widget UI 层, 也就是我们前面看到的 widgetEntryView.

SimpleEntry 继承自 TimelineEntry 用于表示 Widget 中的数据实体对象:

struct SimpleEntry: TimelineEntry {
public let date: Date
}

snapshot 的初步介绍就是这样. 接下来我们再看 timeline.

timeline 给 Widget 提供实际的数据. 系统在调用完 snapshot 提供快照数据后, 然后就会调用 timeline 获取实际的数据.

public func timeline(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)
entries.append(entry)
}

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}

上面是 timeline 的代码, 首先 for hourOffset in 0 ..< 5 { 进行了一个 5次循环:

let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate)
entries.append(entry)

循环体中, 每次给 entryDate 增加1个小时, 然后将他们保存到 entries 集合中. 最后把这些集合添加到 Timeline 里:

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)

policy: .atEnd 是 Timeline 的加载策略. 我们一共给 entries 放入了 5 个实例, 每个实例的时间都递增 1 小时. 也就是说, 每隔 1个小时, Widget 就会刷新它的界面, 然后 5次之后 entries 中的所有实例都被显示一遍.

因为我们设置了 policy: .atEnd, 就代表 5 次刷新结束后, 重新填充 Timeline. 再次调用我们上面的代码, 插入 5 的新的实例, 如此循环, 最终的效果就是每隔 1个小时, 我们的 Widget 就更新一下 UI 显示.

总结

篇幅原因, 我希望每次文章尽量不占用大家过多时间, 这样也能保证最好的阅读效率. 这篇主要介绍 Widget 最基本,也是最核心的内容. 再回顾下 Widget 的主入口:

@main
struct widget: Widget {

private let kind: String = "widget"

public var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
widgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}

再来看看几个主要组件的关系:

以上就是对 Widget 的初步概览了. 如果大家有问题或者建议, 又或者发现我写的有理解不对的地方. 欢迎大家留言. 如果觉得文章对你有用, 也欢迎转发给朋友.


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe