「Apple Watch 應用開發系列」複雜功能實踐

語言: CN / TW / HK

在本節,我們將實現一個簡單的複雜功能。

新建項目

新建項目,RandomAddress,字如其名,我們後續可能將在 App 中獲取隨機地址:

![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5600b4c6786249f5b2eec10da4998d59~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/054ff36c592c46aa81ca6cdbca1ad048~tplv-k3u1fbpfcp-watermark.image?)

Complication DataSource

在項目中新增文件 ComplicationController.swift

```Swift import ClockKit

class ComplicationController: NSObject, CLKComplicationDataSource { func currentTimelineEntry( for complication: CLKComplication ) async -> CLKComplicationTimelineEntry? { return nil } } ```

在 Xcode 14 之前的版本,當我們創建一個 watchOS 項目時,Xcode 會自動生成 ComplicationController.swift。而 Xcode14 後,我們需要手動進行創建。如果用的是 Xcode 14 之前的版本,我們會發現在上述代碼中,刪除了除 CLKComplicationDataSource 所需的一種方法之外的所有內容。大多數樣板代碼都是不必要的,這裏為了簡單起見,我們不進行保留。

當前 Timeline entry

當 watchOS 想要更新複雜功能顯示的數據時,它會調用 currentTimelineEntry(for:)。我們應該立即返回要顯示的數據或返回 nil。

如果我們無法提供當前時間的數據,watchOS 將在 App 的擴展程序的 Assets.xcassets 中查找。如果你使用 Xcode 14 前的版本,可能已經注意到 Assets.xcassets 中有一個之前沒有使用過的 Complication 文件夾。

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

currentTimelineEntry(for:) 返回 nil 時,watchOS 將使用Assets.xcassets 中存在的適當命名的圖像。

模擬器的默認錶盤是 Meridian,它使用 .graphicCircular 複雜功能系列來處理大多數可配置的複雜功能。對於你第一次嘗試在 App 中支持複雜功能,請將方法主體替換為:

Swift func currentTimelineEntry( for complication: CLKComplication ) async -> CLKComplicationTimelineEntry? { guard complication.family == .circularSmall else { return nil } let template = CLKComplicationTemplateGraphicCircularStackText( line1TextProvider: .init(format: "line1"), line2TextProvider: .init(format: "line2")) return .init(date: Date(), complicationTemplate: template) }

在上述代碼中:

  1. 如果是不支持的複雜功能系列類型,則返回 nil。

  2. 創建適當類型的複雜功能模板並配置要顯示的文本。

  3. 返回一個 CLKComplicationTimelineEntry,它指定數據的時間和要顯示的模板。指定的日期不應該是將來,但它可以是過去。

在第二步中,請注意模板採用裏兩個文本。每個模板使用不同的文本或圖形元素,請查閲各種模板的文檔以確定哪些適合我們的。

配置複雜功能

如果你使用的是 Xcode 14 及以上版本,需要手動配置 Scheme。我們新建一個 Schema,並修改對應的名字:

![image](http://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1d1bcaff99cb482d84da221d4e32f924~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/938d2fb48d3548b8b03983a833517726~tplv-k3u1fbpfcp-watermark.image?)

接着編輯我們的 Scheme,將 Interface 修改為 Completion:

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

打開項目的 Info,新增 ClockKit Complication - Principal Class Key,並將 Value 設置為 $(PRODUCT\_MODULE\_NAME).ComplicationController

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

展示覆雜功能

將 Scheme 換到 Complication,然後再次構建並運行。使用該 Scheme 可確保在不使用緩存的情況下使用 App 支持的系列。該 Scheme 還將模擬器直接啟動到錶盤,併為我們的 App 提供少量後台處理時間。

找一個支持我們剛剛複雜功能代碼的錶盤,長按錶盤,編輯器將出現,向左滑動兩次,以便選擇要替換的複雜功能:

![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e19d708b82764026816e94df26f87508~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c437bbaa2008474e91bf2157c849279d~tplv-k3u1fbpfcp-watermark.image?)
![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5b5d6b2f43294a699e276ff39418a905~tplv-k3u1fbpfcp-watermark.image?) ![image](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a0cac610c53246499aeddbcb343cae05~tplv-k3u1fbpfcp-watermark.image?)

點按我們希望替換的圓形複雜功能,查看可以選擇的複雜功能列表:

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

滾動直到我們看到我們自己的 App,選擇我們剛剛創建的閃亮的新複雜功能。可實際上沒有看到我們的 App。 不好了! 什麼地方出了錯?

CLKComplicationDataSource 有一個名為 complicationDescriptors() 的可選方法。 這其實不是“可選的”。 如果我們不提供該方法,我們將不會看到列出的複雜功能。

早起版本的 watchOS 在 Info.plist 中查找支持的複雜功能。這就是為什麼該方法是可選的。 根據 Apple 的建議,不要再使用 Info.plist。

在 ComplicationController.swift 裏,繼續添加以下代碼:

Swift func complicationDescriptors() async -> [CLKComplicationDescriptor] { return [ .init(identifier: "layer.practice.RandomAddress", displayName: "RandomAddress", supportedFamilies: [.graphicCircular]) ] }

在上述代碼中:

  1. 我們提供一個 CLKComplicationDescriptor 項數組作為此方法的返回值。 每個 CLKComplicationDescriptor 都出現在可供選擇的複雜功能列表中。

  2. 我們支持的每個複雜功能功能都應該有一個唯一的名稱。 確保名稱是確定性的,並且不會在應用程序啟動之間發生變化。

  3. displayName 是用户從應用支持的列表中選擇複雜功能時看到的內容。

  4. 複雜功能提供了他們支持的系列。

再次構建並運行。 這一次,當我們滾動複雜功能時,會看到我們的複雜功能被列為可供選擇的選項。

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

圈子裏的“--”是怎麼回事? 為什麼它沒有顯示我們在時間軸中指定的消息?

樣本數據

currentTimelineEntry 既不會顯示在此列表中,也不會顯示在 iPhone 上的 Watch App 中。 當詢問 currentTimelineEntry 時,我們的 App 可能必須執行昂貴的操作或異步運行某些東西。使用這裏的數據作為樣本數據是不妥的。

我們將使用一組樣本數據來使顯示,並避免執行其他代碼,帶來 App 中的潛在副作用。 CLKComplicationDataSource 提供了另一個可選的——名為 localizableSampleTemplate(for:) 的方法,當 Apple Watch 需要在列表中顯示覆雜選擇器時 watchOS 調用該方法。

繼續添加以下代碼:

Swift func localizableSampleTemplate( for complication: CLKComplication ) async -> CLKComplicationTemplate? { guard complication.family == .graphicCircular, let image = UIImage(systemName: "xmark")?.withTintColor(.white) else { return nil } return CLKComplicationTemplateGraphicCircularStackImage( line1ImageProvider: .init(fullColorImage: image), line2TextProvider: .init(format: "hhh")) }

在該方法中:

  1. 它確係列是我們支持的系列。 同時確保你可以加載默認圖像以顯示在複雜功能預覽中。

  2. 提供 CLKComplicationTemplateGraphicCircularStackImage 作為樣本數據。

再次構建並運行。 這一次,當我們嘗試選擇複雜功能時,我們會看到更好的顯示。接着擊該行以選擇複雜功能,然後返回 Apple Watch 的主屏幕。我們卻沒有看到我們的複雜功能顯示:

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

更新複雜功能的數據

Apple Watch 只會在我們指定新數據可用時嘗試更新錶盤上的複雜功能。 想象一下,如果 watchOS 必須每秒查詢 App 的複雜功能以查看是否有新數據點可用,會消耗多少電量?

告訴 watchOS 有新數據

新增文件 Model.swift,他將獲取隨機的城市:

```Swift import SwiftUI

struct Address: Decodable { var city: String }

class Model { static let shared = Model() var address: Address?

func getAddress() async {
    let (data, _) = try! await URLSession.shared.data(from: URL(string: "http://random-data-api.com/api/v2/addresses")!)
    address = try! JSONDecoder().decode(Address.self, from: data)
    print(address!.city)
}

} ```

可以嘗試在 ContentView 展示時,拉取該信息:

Swift struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundColor(.accentColor) Text("Hello, world!") } .padding() .task { await Model.shared.getAddress() } } }

切換 Scheme 運行項目,我們會看到控制枱有城市信息輸出:

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

回到 Model.swift:

Swift import ClockKit

修改 Model:

```Swift class Model { static let shared = Model() var address: Address?

func getAddress() async {
    let (data, _) = try! await URLSession.shared.data(from: URL(string: "http://random-data-api.com/api/v2/addresses")!)
    address = try! JSONDecoder().decode(Address.self, from: data)
    print(address!.city)
    DispatchQueue.main.async {
        let server = CLKComplicationServer.sharedInstance()
        server.activeComplications?.forEach {
            server.reloadTimeline(for: $0)
        }
    }
}

} ```

一旦我們獲取到數據,我們告訴 watchOS 它需要重新加載時間線以處理當前錶盤上的任何複雜功能。

根據我們的 App 及其數據模型,重新加載整個時間線可能不是最有效的選擇。如果我們的複雜功能的時間線中的現有數據仍然有效,並且我們只是添加新數據,則應改為調用 extendTimeline(for:)

注意:如果我們已經超出了應用程序的預算的執行時間,那麼對任一方法的調用都不會執行任何操作。

為複雜功能提供數據

切換回 ComplicationController.swift 並將 currentTimelineEntry(for:) 的主體替換為:

Swift func currentTimelineEntry( for complication: CLKComplication ) async -> CLKComplicationTimelineEntry? { guard complication.family == .graphicCircular, let address = Model.shared.address else { return nil } let template = CLKComplicationTemplateGraphicCircularStackImage( line1ImageProvider: .init(fullColorImage: UIImage(systemName: "xmark")!.withTintColor(.white)), line2TextProvider: .init(format: address.city)) return .init(date: Date(), complicationTemplate: template) }

在上述代碼中,我們獲取了 Model 單例裏的 Address 並進行更新。

再次構建並運行。 請稍等片刻,從網絡下載數據,然後切換回錶盤。 我們將看到現在顯示的真實數據:

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

支持多個系列

雖然我們現在擁有一個支持複雜功能的 App,但它的功能非常有限。為了讓用户使用我們的複雜功能,他們必須使用支持 .graphicCircular 的錶盤。每當我們為 Apple Watch 設計複雜功能時,我們都應該努力支持各種類型的系列。

回想一下,當我們在 complicationDescriptors() 中生成CLKComplicationDescriptor 時,我們為 supportedFamilies 參數指定了一個系列。雖然我們需敲幾下鍵就可以添加其餘類型,甚至只需指定 CLKComplicationFamily.allCases,單我們仍然必須處理每個不同的模板類型。

我們在網上看到的大多數資源都告訴我們只需在每種方法中針對系列創建一個 switch 語句來確定要採取的操作。雖然我們可以這樣做,但控制器將變得非常臃腫並且難以維護。有一種常見的設計模式,稱為工廠方法,在這裏,它的效果很好,歡迎嘗試實現。

鏈接

  • 你可以在這裏獲得文章項目:http://github.com/LLLLLayer/Apple-Watch-App-Development-Series

  • 使用 http://random-data-api.com/ 提供的 Fake 數據。