「Apple Watch 应用开发系列」Dock 快照

语言: CN / TW / HK

Dock

Apple Watch 有两个物理按钮。 除了数字表冠,下方还有一个侧面按钮。 如果用户按一下按钮,Dock 将启动。Dock 显示两组应用程序之一:最近运行、收藏夹。可在设置中进行更改:

![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00ce4c4c672f40878d703893a2d9946e~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c5512375a64411796076bb553c0546e~tplv-k3u1fbpfcp-watermark.image?)

大多数用户通过快速按两次以调出 Apple Pay 来与侧边按钮进行交互。Apple Watch 上的 Dock 可让佩戴者查看最近运行的或最喜欢的 App。既然 watchOS 已经在适当的时候显示这些 App,我们为什么要关心 Dock?

Dock 提供多种好处:

  • 点击会立即启动 App。

  • 在应用程序之间快速切换。

  • 一目了然地显示当前的 App 状态。

  • 多个运行过的应用的组织。

虽然 watchOS 会将 App 推入后台时的外观插入 Dock,但它只会显示当前屏幕包含的内容,对于大多数应用程序来说,没有其他必要了。但在 Timer 相关 App 的情况下,单个快照是不够的,可能不是最佳的用户体验。

尝试使用“计时器”App,当我们启动一个倒计时并将其推到后台,查看 Dock,我们将会看到计时任在 Doker 中继续计时。下面,我们将尝试实现类似的功能。

![image](http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a0b1844ebbb4cc484031bd48c510262~tplv-k3u1fbpfcp-watermark.image?)

快照 API

作为开发者,我们有责任告诉 watchOS 在应用程序移至后台后时是否需要执行额外的快照。我们首先将学习一些技巧,如果我们想优化你的 App 快照,我们应该考虑到这些内容。

尺寸优化

快照是 App 全尺寸的缩小图像。请务必仔细查看我们在快照中捕获的内容。文本是否仍然可读? 图像在较小尺寸下是否有意义?需要记住的是:

  • 在较小的尺寸下,较粗的字体更易于阅读。

  • 对于重要信息,我们可能希望使用粗体或更大的字体。

  • 我们可能还需要考虑从屏幕上删除不太重要的元素。

自定义界面

快照只是应用程序当前显示的图像。考虑到这一点,我们可能完全重新设计 UI 时制作自定义视图,但请三思。

在某些情况下拥有自定义视图可能很有意义,尤其是当我们需要从快照中删除某些元素时。如果我们制作自定义视图,我们需要注意确保快照看起来与正常显示没有很大的不同。用户希望快照代表我们的 App,如果快照看起来差别比较大,就很难识别和找到他们正在寻找的 App。

如果我们确定需要不同的视觉效果,请记住以下几点:

  • 专注于重要信息。

  • 隐藏在 Dock 中查看时不相关的对象。

  • 为了便于阅读,放大或缩小某些视图的大小。

  • 不要让界面看起来完全不同。

进度或状态

进度、计时器等是快照的绝佳用例。我们可能想同时展示所有这些内容,但需要需要考虑用户是否想使用我们的复杂功能。

在线订购应用程序也是自定义快照的完美示例。当用户订购食物时,会看到一个视图。 餐厅收到用户的订单后,屏幕可能会发生变化,以显示食物准备好取货的时间。 当司机拿到食物时,屏幕会显示距离送货还有多长时间。

改变场景

尽可能保持当前活动视图与用户上次交互时的相同。如果 App 的状态以不可预知的方式发生变化,可能会使用户感到困惑。Dock 和 App 之间的交互应该无缝,防止用户不了解正在发生的任何特殊情况。

预测用户期望

我们应该预测用户在查看 Dock 时希望看到的内容。考虑一场体育赛事,根据事件发生的时间,用户可能想要这样的东西:

  • 在比赛开始前不久,用户希望看到时间和位置。

  • 在游戏过程中,用户希望看到当前比分。

  • 比赛结束后,用户希望看到最终得分。

  • 其他时候,用户可能希望查看本赛季的时间表。

同时,并非每个用户都希望看到相同的内容。需要考虑 App 能否提供进一步自定义视图的可能性?只关心一支球队的球迷需要与想要关注整个联盟的体育狂热者不同的体验。

快照发生时机

watchOS 在许多不同的场景中自动安排快照来进行更新:

  • Apple Watch 启动时。

  • 复杂功能更新时。

  • App 从前台转换到后台时。

  • 当用户查看 long look 长视图通知时。

  • 用户最后一次与 App 交互后的一小时。

  • Dock 中的应用程序至少每小时一次。

  • 在可选的预定时间,使用后台刷新 API。

http://developer.apple.com/documentation/watchkit/background_execution/preparing_to_take_your_watchos_app_s_snapshot

完成 Timer Demo

框架代码

新建 WatchOnlyApp Timer:

![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd824ce0c74c4dedb96d6bcc82f3400f~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/859357bdc9c44a0b96895ba3860fbf20~tplv-k3u1fbpfcp-watermark.image?)

修改 ContentView,让我们的时间显示出来:

```Swift struct ContentView: View { @State var timeText = "" var timer: Timer { get { Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in let formatter = DateFormatter() formatter.dateFormat = "hh:mm:ss" self.timeText = formatter.string(from: Date()) } } }

var body: some View {
    VStack {
        Text(timeText)
            .onAppear {
                _ = timer
            }
    }
}

}

struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } ```

![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d12fb1840ab4b529fdf4f966f1a00b9~tplv-k3u1fbpfcp-watermark.image?)

快照 handler

接着来到 TimerApp.swift,调整以下代码:

```Swift import SwiftUI

final class ExtensionDelegate: NSObject, WKExtensionDelegate { func handle(_ backgroundTasks: Set) { backgroundTasks.forEach { task in guard let snapshot = task as? WKSnapshotRefreshBackgroundTask else { task.setTaskCompletedWithSnapshot(false) return } print("Taking a snapshot") task.setTaskCompletedWithSnapshot(true) } } }

@main struct Timer_Watch_AppApp: App { @WKExtensionDelegateAdaptor(ExtensionDelegate.self) private var extensionDelegate

var body: some Scene {
    WindowGroup {
        ContentView()
    }
}

} ```

每当 watchOS 从后台唤醒 App 时,它都会调用 handle(_:):

  1. 首先,它会安排一个或多个任务供我们处理,我们循环遍历每一个。

  2. 我们只关心快照,因此如果不是快照,则将任务标记为已完成。传递 false 告诉系统不要自动安排另一个快照。

  3. 最后,你将快照任务标记为完成并指定 true,以便 watchOS 后续自动安排另一个快照。

当 watchOS 调用 handle(_:) 时,我们有有限的时间(大约几秒)来完成任务。如果我们忽略将任务标记为完成,系统将持续在后台运行它。这些的任务将一直运行,直到耗尽所有可用时间,这会浪费电量。

在第二步中,将 false 传递给 setTaskCompletedWithSnapshot,这里不指定 true。由于我们没有对任务执行任何操作,因此没有理由强制立即生成快照。

模拟器运行 App 后,切换到主屏幕。请耐心等待——在一段不确定但可能很短的时间之后,你会看到你的应用拍摄快照的消息。

![image](http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0d53f4c5b4674fbcbd3d76d0d1728fe9~tplv-k3u1fbpfcp-watermark.image?)

强制快照

当我们切换到主屏幕时,快照任务并没有立即运行,因为 watchOS 正忙于执行其他任务。这对于正常的生产部署可能没问题,但是在构建应用程序时,我们会希望能够告诉模拟器立即拍摄快照。

请记住,仅当 App 不在前台时才会发生快照。模拟器或物理设备必须显示任何其他 App 或表盘。当然,该 App 必须通过 Xcode 运行才能让我们看到。

一旦我们的 App 在后台运行,在 Xcode 的菜单栏中,选择 Debug ▸ Simulate UI Snapshot。

![image](http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/753a4665555e4b2f8fc2e06505b45375~tplv-k3u1fbpfcp-watermark.image?)

我们将再次在 Debug 区域中看到该消息,让我们知道 watchOS 拍摄了快照。请注意表盘上的其他任何内容都没有变化。快照是一项后台任务,这意味着 watchOS 没有理由让 App 引起用户的注意。

查看快照

我们可以通过访问 Dock 查看快照的外观。调出 Dock 后,会看到 App 是列表中的第一个。但这并不能让我们很好地查看快照, 运行任何其他应用程序,然后跳回 Dock。 我们将更好地查看快照:

![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f9828d7c49bf4361b8e7d3fde3db53b3~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dfc41d0270b140daa65dcaf9f3b306ce~tplv-k3u1fbpfcp-watermark.image?)

自定义快照

如前所述,App 的快照默认为可见的最后一个屏幕。

切换到 ExtensionDelegate.swift 并添加以下方法:

Swift private func nextSnapshotDate() -> Date { let twoDaysLater = Calendar.current.date( byAdding: .day, value: 2, to: Date() )! return Calendar.current.startOfDay(for: twoDaysLater) }

使用日历计算,我们可以确定两天后的日期。此计算不会失败,因此强制解包返回值是安全的。最后,我们确定发生快照的一天的开始时间。

快照 API 基于字典类型的 Userinfo。虽然我们可以使用字典,但有更好的方法来处理数据。我们不希望必须对 key 的字符串进行硬编码或定义全局 let 类型常量。相反,创建一个名为 SnapshotUserInfo.swift 的新 Swift 文件。当快照发生时,我们必须根据之前定义的规则将应用程序切换到适当的屏幕。

```Swift import Foundation

struct SnapshotUserInfo { let handler: () -> Void let content: String } ```

我们必须在完成时告诉快照,稍后会详细介绍。ContentView.Destination 是一个枚举,标识哪个视图将被推送到导航堆栈上。

接下来,实现 SnapshotUserInfo 的初始化程序:

Swift init( handler: @escaping () -> Void, content: String ) { self.handler = handler self.content = content }

虽然不是必需的,但实现初始化程序可以让我们初始化结构实例,而无需为匹配显式指定 nil。

现在我们需要一种方法来生成 Snapshot API 需要的字典。 继续添加代码:

```Swift private enum Keys: String { case handler, content }

func encode() -> [AnyHashable: Any] { return [ Keys.handler.rawValue: handler, Keys.content.rawValue: content, ] } ```

我们定义一个枚举来存储字典的 key。 创建一个将结构编码为 Snapshot API 所需的字典类型的方法。

然后在 struct 内部,实现从 notification API 的字典信息转换的方法:

Swift static func from(notification: Notification) throws -> Self { guard let userInfo = notification.userInfo else { throw SnapshotError.noUserInfo } guard let handler = userInfo[Keys.handler.rawValue] as? () -> Void else { throw SnapshotError.noHandler } guard let content = userInfo[Keys.content.rawValue] as? String else { throw SnapshotError.badDestination } return .init( handler: handler, content: content ) }

在 handle(_:) 方法中,删除完成快照的最后一行并将其替换为:

Swift let nextSnapshotDate = nextSnapshotDate() let handler = { snapshot.setTaskCompleted( restoredDefaultState: false, estimatedSnapshotExpiration: nextSnapshotDate, userInfo: nil ) }

当 watchOS 拍摄快照时,我们告诉它完成任务。我们将 false 传递给第一个参数,因为你让应用程序处于与最初不同的状态。第二个参数 estimatedSnapshotExpiration 告诉 watchOS 当前快照何时不再有效。 本质上,我们提供的日期是它需要拍摄新快照的时间。最后,对于 userInfo 参数,只需传递 nil,因为我们不需要下一个快照来了解有关应用程序当前状态的任何信息。否则 userInfo 就是你可以传递一些东西的地方。

继续添加到 handle(_:):

Swift let snapshotUserInfo = SnapshotUserInfo( handler: handler, content: "我被快照啦!" ) NotificationCenter.default.post( name: Notification.Name("snapshot"), object: nil, userInfo: snapshotUserInfo.encode() )

我们创建了一个 SnapshotUserInfo 类型的常量,使用该对象发布通知。

回到 ContentView,调整代码,我们接受前文的通知,若收到通知,则展示新文本,更新代码:

```Swift struct ContentView: View { @State var timeText = "" @State var snapshotText: String?

private let pushViewForSnapshotPublisher = NotificationCenter
  .default
  .publisher(for: Notification.Name("snapshot"))

var timer: Timer {
    get {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            let formatter = DateFormatter()
            formatter.dateFormat = "hh:mm:ss"
            self.timeText = formatter.string(from: Date())
        }
    }
}

var body: some View {
    VStack {
        Text(timeText)
            .onAppear {
                _ = timer
            }
        if snapshotText != nil {
            Text(snapshotText!)
        }
    }
    .onReceive(pushViewForSnapshotPublisher) { notification in
        guard let info = try? SnapshotUserInfo.from(notification: notification) else {
          return
        }
        self.snapshotText = info.content
        info.handler()
    }
}

} ```

运行并触发快照,我们的视图被更新了:

![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f904b957c5844281bb37c79c2d76b2e6~tplv-k3u1fbpfcp-watermark.image?)

你可以从这里获取文章中的源码。