「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。

https://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?)

你可以從這裏獲取文章中的源碼。