「Apple Watch 應用開發系列」Dock 快照
Dock
Apple Watch 有兩個物理按鈕。 除了數字表冠,下方還有一個側面按鈕。 如果使用者按一下按鈕,Dock 將啟動。Dock 顯示兩組應用程式之一:最近執行、收藏夾。可在設定中進行更改:
大多數使用者通過快速按兩次以調出 Apple Pay 來與側邊按鈕進行互動。Apple Watch 上的 Dock 可讓佩戴者檢視最近執行的或最喜歡的 App。既然 watchOS 已經在適當的時候顯示這些 App,我們為什麼要關心 Dock?
Dock 提供多種好處:
-
點選會立即啟動 App。
-
在應用程式之間快速切換。
-
一目瞭然地顯示當前的 App 狀態。
-
多個執行過的應用的組織。
雖然 watchOS 會將 App 推入後臺時的外觀插入 Dock,但它只會顯示當前螢幕包含的內容,對於大多數應用程式來說,沒有其他必要了。但在 Timer 相關 App 的情況下,單個快照是不夠的,可能不是最佳的使用者體驗。
嘗試使用“計時器”App,當我們啟動一個倒計時並將其推到後臺,檢視 Dock,我們將會看到計時任在 Doker 中繼續計時。下面,我們將嘗試實現類似的功能。
快照 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:
修改 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() } } ```
快照 handler
接著來到 TimerApp.swift,調整以下程式碼:
```Swift import SwiftUI
final class ExtensionDelegate: NSObject, WKExtensionDelegate {
func handle(_ backgroundTasks: Set
@main struct Timer_Watch_AppApp: App { @WKExtensionDelegateAdaptor(ExtensionDelegate.self) private var extensionDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
} ```
每當 watchOS 從後臺喚醒 App 時,它都會呼叫 handle(_:):
-
首先,它會安排一個或多個任務供我們處理,我們迴圈遍歷每一個。
-
我們只關心快照,因此如果不是快照,則將任務標記為已完成。傳遞 false 告訴系統不要自動安排另一個快照。
-
最後,你將快照任務標記為完成並指定 true,以便 watchOS 後續自動安排另一個快照。
當 watchOS 呼叫 handle(_:) 時,我們有有限的時間(大約幾秒)來完成任務。如果我們忽略將任務標記為完成,系統將持續在後臺執行它。這些的任務將一直執行,直到耗盡所有可用時間,這會浪費電量。
在第二步中,將 false 傳遞給 setTaskCompletedWithSnapshot,這裡不指定 true。由於我們沒有對任務執行任何操作,因此沒有理由強制立即生成快照。
模擬器執行 App 後,切換到主螢幕。請耐心等待——在一段不確定但可能很短的時間之後,你會看到你的應用拍攝快照的訊息。
強制快照
當我們切換到主螢幕時,快照任務並沒有立即執行,因為 watchOS 正忙於執行其他任務。這對於正常的生產部署可能沒問題,但是在構建應用程式時,我們會希望能夠告訴模擬器立即拍攝快照。
請記住,僅當 App 不在前臺時才會發生快照。模擬器或物理裝置必須顯示任何其他 App 或錶盤。當然,該 App 必須通過 Xcode 執行才能讓我們看到。
一旦我們的 App 在後臺執行,在 Xcode 的選單欄中,選擇 Debug ▸ Simulate UI Snapshot。
我們將再次在 Debug 區域中看到該訊息,讓我們知道 watchOS 拍攝了快照。請注意錶盤上的其他任何內容都沒有變化。快照是一項後臺任務,這意味著 watchOS 沒有理由讓 App 引起使用者的注意。
檢視快照
我們可以通過訪問 Dock 檢視快照的外觀。調出 Dock 後,會看到 App 是列表中的第一個。但這並不能讓我們很好地檢視快照, 執行任何其他應用程式,然後跳回 Dock。 我們將更好地檢視快照:
自定義快照
如前所述,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()
}
}
} ```
執行並觸發快照,我們的檢視被更新了:
你可以從這裡獲取文章中的原始碼。
- 深入淺出RPC服務 | 不同層的網路協議
- 淺析三款大規模分散式檔案系統架構設計
- 動轉靜兩大升級!一鍵轉靜成功率領先,重點模型訓練提速18%
- 文心ERNIE 3.0 Tiny新升級!端側壓縮部署“小” “快” “靈”!
- [Android禪修之路] 解讀Layer
- 35張圖,直觀理解Stable Diffusion
- Combine | (V) Combine 中的錯誤處理和 Scheduler
- Combine | (III) Combine Operator:時間操作|序列
- 「Apple Watch 應用開發系列」複雜功能(Complication)基礎
- 「Apple Watch 應用開發系列」複雜功能實踐
- 在 iOS 上實現使用者主動觸發的 App Icon 切換
- view和layer知識點整理
- 實時活動(Live Activity) - 在鎖定螢幕和靈動島上顯示應用程式的實時資料
- 「Apple Watch 應用開發系列」Dock 快照
- Layer 1新方向 Meta系公鏈Aptos、Sui 、Linera盤點
- Synapse Chain:多鏈世界的Layer0跨鏈基礎設施
- V神:什麼樣的layer 3才有意義?
- 以太坊升級將如何影響Layer2的發展?
- Vitalik:什麼樣的 Layer3 才有意義
- 從系統架構分析安全問題及應對措施