「Apple Watch 应用开发系列」复杂功能实践

2,872 阅读4分钟

在本节,我们将实现一个简单的复杂功能。

新建项目

新建项目,RandomAddress,字如其名,我们后续可能将在 App 中获取随机地址:

image

image

Complication DataSource

在项目中新增文件 ComplicationController.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

currentTimelineEntry(for:) 返回 nil 时,watchOS 将使用Assets.xcassets 中存在的适当命名的图像。

模拟器的默认表盘是 Meridian,它使用 .graphicCircular 复杂功能系列来处理大多数可配置的复杂功能。对于你第一次尝试在 App 中支持复杂功能,请将方法主体替换为:

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

image

接着编辑我们的 Scheme,将 Interface 修改为 Completion:

image

打开项目的 Info,新增 ClockKit Complication - Principal Class Key,并将 Value 设置为 $(PRODUCT\_MODULE\_NAME).ComplicationController

image

image

展示复杂功能

将 Scheme 换到 Complication,然后再次构建并运行。使用该 Scheme 可确保在不使用缓存的情况下使用 App 支持的系列。该 Scheme 还将模拟器直接启动到表盘,并为我们的 App 提供少量后台处理时间。

找一个支持我们刚刚复杂功能代码的表盘,长按表盘,编辑器将出现,向左滑动两次,以便选择要替换的复杂功能:

image

image

image

image

点按我们希望替换的圆形复杂功能,查看可以选择的复杂功能列表:

image

滚动直到我们看到我们自己的 App,选择我们刚刚创建的闪亮的新复杂功能。可实际上没有看到我们的 App。 不好了! 什么地方出了错?

CLKComplicationDataSource 有一个名为 complicationDescriptors() 的可选方法。 这其实不是“可选的”。 如果我们不提供该方法,我们将不会看到列出的复杂功能。

早起版本的 watchOS 在 Info.plist 中查找支持的复杂功能。这就是为什么该方法是可选的。 根据 Apple 的建议,不要再使用 Info.plist。

在 ComplicationController.swift 里,继续添加以下代码:

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

在上述代码中:

  1. 我们提供一个 CLKComplicationDescriptor 项数组作为此方法的返回值。 每个 CLKComplicationDescriptor 都出现在可供选择的复杂功能列表中。

  2. 我们支持的每个复杂功能功能都应该有一个唯一的名称。 确保名称是确定性的,并且不会在应用程序启动之间发生变化。

  3. displayName 是用户从应用支持的列表中选择复杂功能时看到的内容。

  4. 复杂功能提供了他们支持的系列。

再次构建并运行。 这一次,当我们滚动复杂功能时,会看到我们的复杂功能被列为可供选择的选项。

image

圈子里的“--”是怎么回事? 为什么它没有显示我们在时间轴中指定的消息?

样本数据

currentTimelineEntry 既不会显示在此列表中,也不会显示在 iPhone 上的 Watch App 中。 当询问 currentTimelineEntry 时,我们的 App 可能必须执行昂贵的操作或异步运行某些东西。使用这里的数据作为样本数据是不妥的。

我们将使用一组样本数据来使显示,并避免执行其他代码,带来 App 中的潜在副作用。 CLKComplicationDataSource 提供了另一个可选的——名为 localizableSampleTemplate(for:) 的方法,当 Apple Watch 需要在列表中显示复杂选择器时 watchOS 调用该方法。

继续添加以下代码:

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

image

更新复杂功能的数据

Apple Watch 只会在我们指定新数据可用时尝试更新表盘上的复杂功能。 想象一下,如果 watchOS 必须每秒查询 App 的复杂功能以查看是否有新数据点可用,会消耗多少电量?

告诉 watchOS 有新数据

新增文件 Model.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: "https://random-data-api.com/api/v2/addresses")!)
        address = try! JSONDecoder().decode(Address.self, from: data)
        print(address!.city)
    }
}

可以尝试在 ContentView 展示时,拉取该信息:

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

回到 Model.swift:

import ClockKit

修改 Model:

class Model {
    static let shared = Model()
    var address: Address?
    
    func getAddress() async {
        let (data, _) = try! await URLSession.shared.data(from: URL(string: "https://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:) 的主体替换为:

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

支持多个系列

虽然我们现在拥有一个支持复杂功能的 App,但它的功能非常有限。为了让用户使用我们的复杂功能,他们必须使用支持 .graphicCircular 的表盘。每当我们为 Apple Watch 设计复杂功能时,我们都应该努力支持各种类型的系列。

回想一下,当我们在 complicationDescriptors() 中生成CLKComplicationDescriptor 时,我们为 supportedFamilies 参数指定了一个系列。虽然我们需敲几下键就可以添加其余类型,甚至只需指定 CLKComplicationFamily.allCases,单我们仍然必须处理每个不同的模板类型。

我们在网上看到的大多数资源都告诉我们只需在每种方法中针对系列创建一个 switch 语句来确定要采取的操作。虽然我们可以这样做,但控制器将变得非常臃肿并且难以维护。有一种常见的设计模式,称为工厂方法,在这里,它的效果很好,欢迎尝试实现。

链接