WWDC Quick Look 💓 By SwiftGGTeam
Create complications for Apple Watch

Create complications for Apple Watch

观看原视频

Highlight

watchOS 7 让一个 App 提供多个 Complication,并允许部分 Graphic template 使用 SwiftUI 视图,同时仍由 ClockKit timeline、family 和 template 驱动表盘内容。

核心内容

Apple Watch App 不能只等用户点开。表盘才是用户最常看的位置,Complication 把 App 的最新状态放到那里:下一班船、当前比分、倒计时、今日进度,都可以在抬腕时出现(00:23)。

ClockKit 的做法是先让 App 交出一条 timeline。每个 entry 有一个生效时间和一个 template;系统按时间自动切换 entry,不需要 App 一直运行。Session 用观鲸行程解释这个模型:行程本来有起止时间,但 Complication entry 只绑定一个时间,并且可以提前于真实开始时间出现,给用户留出赶船时间(01:33)。

这场 session 还把 watchOS 7 的两个变化放进同一条链路:Graphic Extra Large family 新增到 Extra Large 表盘;一个 App 可以提供多个 Complication。Whale Watch 示例里,同一个 App 同时提供不同观测站的目击信息、记录新目击的入口、以及整体季节数据(03:0511:47)。

SwiftUI 在这里不是替代全部 ClockKit。演讲说的是:使用 CLKFullColorImageProvider 的 complication templates 有 SwiftUI view alternatives。开发者仍然需要提供 timeline、选择 family、返回 template,只是在部分 Graphic template 中可以把 SwiftUI view 放进去(11:15)。

详细内容

用 timeline 交出当前和未来状态

04:54CLKComplicationDataSource 只有一个必需方法:getCurrentTimelineEntry(for:withHandler:)。它回答“现在表盘应该显示什么”。

// CLKComplicationDataSource - Required
class ComplicationController: NSObject, CLKComplicationDataSource {

    func getCurrentTimelineEntry(
        for complication: CLKComplication,
        withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void)
    {
        // Call the handler with the current timeline entry
        handler(createTimelineEntry(forComplication: complication, date: Date()))
    }
}

关键点:

  • ComplicationController 遵守 CLKComplicationDataSource,ClockKit 会通过它请求内容。
  • complication 表示表盘上的一个具体实例,后面会用它区分 family 和 identifier。
  • handler 接收一个 CLKComplicationTimelineEntry?,entry 里包含生效时间和 template。
  • createTimelineEntry(forComplication:date:) 是 App 自己的封装,把当前时间转成表盘可显示的 entry。

05:16)如果 App 可以预先提供未来数据,还要实现 timeline end date 和 after-date entries。ClockKit 会按 limit 分批请求,超出 limit 的 entry 会被丢弃。

// CLKComplicationDataSource - Timeline Support
extension ComplicationController {

    func getTimelineEndDate(
        for complication: CLKComplication,
        withHandler handler: @escaping (Date?) -> Void)
    {
        handler(timeline(for: complication)?.endDate)
    }

    func getTimelineEntries(
        for complication: CLKComplication,
        after date: Date,
        limit: Int,
        withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void)
    {
       handler(timeline(for: complication)?.entries(after: date, limit: limit))
    }
}

关键点:

  • getTimelineEndDate 告诉系统这条 timeline 能覆盖到什么时候。
  • getTimelineEntries 从系统已有的最后一个 entry 之后继续提供数据。
  • date 是系统已经拥有的最后一个 timeline entry 时间。
  • limit 是系统本次需要的最大数量,App 不应该一次塞入更多 entry。

当数据完全失效时,调用 CLKComplicationServerreloadTimeline(for:)。如果已有 entry 仍有效,只是 App 能提供更多未来 entry,就调用 extendTimeline(for:)。系统只跟踪当前在用户表盘上的 Complication,这些实例从 activeComplications 或 data source 方法参数里取得,不要自己创建 CLKComplication 对象(06:00)。

用 provider 适配极小空间

07:47)Complication 的空间很小,同一段文字要出现在不同 family 和 template 中。ClockKit 用 data providers 表达数据,再替开发者处理不同布局下的显示。

let longDate: Date = DateComponents(year: 2020, month: 9, day: 23).date ?? Date()
let units: NSCalendar.Unit = [.weekday, .month, .day]
let textProvider = CLKDateTextProvider(date: longDate, units: units)

关键点:

  • longDate 是要显示的日期。
  • units 指定最长情况希望展示 weekday、month、day。
  • CLKDateTextProvider 会根据空间从完整日期逐步降级到更短文本。

08:49)相对时间用 CLKRelativeDateTextProvider。演讲给出的例子是计时器风格,它会随当前时间自动更新,可以精确到秒,不需要 App 额外刷新字符串。

let timerStart: Date =
let units: NSCalendar.Unit = [.hour, .minute, .second]
let textProvider = CLKRelativeDateTextProvider(date: timerStart, style: .timer, units: units)

关键点:

  • timerStart 是相对时间计算的目标时间。
  • style: .timer 让 provider 按计时器形式展示。
  • units 控制展示小时、分钟、秒。
  • 自动更新时间由 ClockKit 处理,适合倒计时和剩余时间。

同一段里还提到 CLKTimeTextProviderCLKTimeIntervalTextProviderCLKSimpleTextProviderCLKImageProviderCLKFullColorImageProviderCLKTimeIntervalGaugeProvider。这些 provider 的共同目标是让内容在不同表盘颜色、不同 family、不同空间大小下仍能被系统正确压缩和着色(09:0110:42)。

用 descriptor 声明多个 Complication

12:07)watchOS 7 新增 multiple complication support。相关方法是 getComplicationDescriptors(withHandler:)handleSharedComplicationDescriptors(_:)CLKComplicationDescriptor 包含唯一 identifier、编辑界面展示的 displayName、支持的 families,以及可选的 userInfouserActivity

// CLKComplicationDataSource - Multiple Complication Support
extension ComplicationController {
    var descriptors : [CLKComplicationDescriptor] = []
    var dataDict = Dictionary<AnyHashable, Any>()

    for station in data.stations {
        dataDict = [“name": station.name, “shortName": station.shortName]
        descriptors.append(
            CLKComplicationDescriptor(
                identifier: station.name,
                displayName: station.name,
                supportedFamilies: CLKComplicationFamily.allCases,
                userInfo: dataDict))
    }

    descriptors.append(
        CLKComplicationDescriptor(
            identifier: "LogSighting",
            displayName: "Log Sighting",
            supportedFamilies: CLKComplicationFamily.allCases))

    descriptors.append(
        CLKComplicationDescriptor(
            identifier: "SeasonData",
            displayName: "Season Data",
            supportedFamilies: [.graphicRectangular]))

    // Call the handler with the currently supported complication descriptors
    handler(descriptors)
}

关键点:

  • 每个 station 都变成一个 descriptor,identifier 和 displayName 使用 station 名称。
  • userInfonameshortName 这类业务数据带到之后的 template 创建阶段。
  • LogSighting 是一个快速记录新目击的 Complication,支持所有 family。
  • SeasonData 只支持 .graphicRectangular,因为整体季节数据需要较大的画布。
  • descriptor 列表变化时,调用 reloadComplicationDescriptors(),系统会重新请求支持列表。

如果用户当前表盘上还放着某个 App 已经不再支持的 Complication,系统仍会继续请求它的 timeline entries。Session 要求开发者尽量继续给出有用内容。另一个必须处理的是 CLKDefaultComplicationIdentifier:旧版 Complication、共享表盘丢失关联数据时,系统可能会用这个 identifier 请求内容(14:1715:59)。

按 family 和 identifier 返回 template

17:33createTimelineEntry 先创建 template,再把 template 和日期包装成 CLKComplicationTimelineEntry

func createTimelineEntry(
    forComplication complication: CLKComplication,
    date: Date) -> CLKComplicationTimelineEntry?
{
    guard let template = createTemplate(forComplication: complication, date: date) else {
        return nil
    }
    return CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
}

关键点:

  • createTemplate 决定具体显示样式。
  • template 创建失败时返回 nil,不给系统无效 entry。
  • 成功时,date 决定 entry 何时开始显示。
  • complicationTemplate 承载最终交给表盘的视觉内容。

17:44)Whale Watch 示例按 (complication.family, complication.identifier) 分支。SeasonData 在 Graphic Rectangular 里返回 SwiftUI chart;LogSighting 在 Graphic Circular 里返回图片加文字;其他观测站根据 family 返回不同 template;无法处理时退回 default template。

func createTemplate(
    forComplication complication: CLKComplication,
    date: Date) -> CLKComplicationTemplate?
{
    var station: Station? = nil
    if let stationName = complication.userInfo?["name"] as? String {
        station = data.stations.first(where: { $0.name == stationName })
    }

    let image = UIImage(named: "Spout-small")!
    let spoutFullColorImageProvider = CLKFullColorImageProvider(fullColorImage: image)
    let logSightingTextProvider = CLKSimpleTextProvider(
        text: "Log Sighting",
        shortText: "Log")

    let defaultTemplate: (CLKComplicationFamily) -> CLKComplicationTemplate = { family -> CLKComplicationTemplate in
      // Return a default complication template for the given family
    }

    switch (complication.family, complication.identifier) {

    case (.graphicRectangular, "SeasonData"):
        return CLKComplicationTemplateGraphicRectangularFullView(
            ChartView(
                seriesData: data.last7DaysSightings,
                seriesColor: .turquoise)

    case (.graphicCircular, "LogSighting"):
        return CLKComplicationTemplateGraphicCircularStackImage(
            line1ImageProvider: spoutFullColorImageProvider,
            line2TextProvider: logSightingTextProvider)

    case (.graphicCircular, _):
        guard let station = station else { return defaultTemplate(.graphicCircular) }
        return CLKComplicationTemplateGraphicCircularView(
            SightingTypeView(station: station))

    case (.graphicCorner, _):
        guard let station = station else { return defaultTemplate(.graphicCorner) }
        return CLKComplicationTemplateGraphicCornerTextImage(
            textProvider: station.timeAndShortLocTextProvider,
            imageProvider: station.whaleActivityFullColorProvider)

    case (.graphicExtraLarge, _):
        guard let station = station else { return defaultTemplate(.graphicExtraLarge) }
        return CLKComplicationTemplateGraphicExtraLargeCircularStackText(
            line1TextProvider: station.timeAndLocationTextProvider,
            line2TextProvider: station.shortLocationTextProvider)

    default:
        return defaultTemplate(complication.family)
    }
}

关键点:

  • complication.userInfo?["name"] 读取 descriptor 带来的 station 名称。
  • CLKFullColorImageProvider 用于 Graphic family 的全彩图像。
  • CLKSimpleTextProviderLogSighting 同时提供长文本和短文本。
  • switch 同时看 family 和 identifier,避免同一个 identifier 在不同 family 中套错 template。
  • default template 处理未知情况,包括默认 Complication identifier。

核心启发

1. 做一个“下一次行动”表盘入口

  • 做什么:把下一次会议、下一班车、下一次服药或下一次潮汐放到表盘上,并让 entry 提前于真实开始时间出现。
  • 为什么值得做:session 的观鲸时间线说明,Complication entry 不必等事件开始才显示,可以提前给用户准备时间。
  • 怎么开始:把业务事件转成 CLKComplicationTimelineEntry,用 getTimelineEntries(after:limit:withHandler:) 提供未来 entry。

2. 为同一个 App 拆出多个数据维度

  • 做什么:一个运动 App 同时提供“当前心率”“今日进度”“开始记录”三个 Complication。
  • 为什么值得做:watchOS 7 允许单个 App 声明多个 CLKComplicationDescriptor,Whale Watch 示例也把观测站、记录入口、季节数据拆开。
  • 怎么开始:在 getComplicationDescriptors(withHandler:) 中为每个维度分配 identifier,并用 userInfo 携带后续渲染需要的数据。

3. 用 provider 把同一份内容适配多个 family

  • 做什么:同一条倒计时在大表盘上显示完整时间,在小角标里自动缩短。
  • 为什么值得做CLKDateTextProviderCLKRelativeDateTextProvider 会根据空间选择合适文本,减少手写布局分支。
  • 怎么开始:先把文本、图片、gauge 都封装成 ClockKit provider,再把 provider 交给对应 template。

4. 给共享表盘和旧用户准备 fallback

  • 做什么:当用户从旧版 Complication 或共享表盘进入时,仍显示 App 图标、热门数据或原有默认信息。
  • 为什么值得做:session 明确要求支持 CLKDefaultComplicationIdentifier,否则用户可能在表盘上看到损坏的 Complication。
  • 怎么开始:在 template 创建逻辑的 default 分支返回一个可识别的默认 template,不要直接返回空内容。

5. 把 SwiftUI 用在最需要画布的位置

  • 做什么:为矩形 Graphic family 做一个小图表,比如最近 7 天记录、趋势或完成度。
  • 为什么值得做:session 示例在 SeasonData.graphicRectangular 分支使用 CLKComplicationTemplateGraphicRectangularFullView 承载 SwiftUI chart。
  • 怎么开始:先限定一个只支持 .graphicRectangular 的 descriptor,再在 (family, identifier) 分支里返回对应 SwiftUI template。

关联 Session

评论

GitHub Issues · utterances