AppleWatchのComplications実装メモ

SurfTideΔSurfTideWatchに、AppleWatchのコンプリケーションを実装したときのメモです。自分用の備忘録なので、それほど親切には書いていませんが、ネットの情報の不足分を補完する感じです。

タイド情報(潮汐)は常に変化するので、以下のような条件でシンプルに実装しています。

  • 一定時間で更新する必要がある
  • 毎回タイドを計算するのは冗長なので当日と前後1日分のデータはキャッシュする
  • 複数の種類のコンプリケーションを準備する

で、まずはXCODEプロジェクトのWatchExtensionに、コンプリケーションを利用するよう設定します。
というのも、自分の実装では「ComplicationController.swift」関連の設定が無かったり(古いプロジェクト)自分で削除していたりしたので、まずはそれを正しく構成しなければなりませんでした。

まずは「Info.plist」にコンプリケーション関連の設定を追加。
必要なKeyは以下2つです。「CLKComplicationPrincipalClass」は「ComplicationController.swift」のパスを指定します。「CLKComplicationSupportedFamilies」は使うタイプのComplicationsだけ設定します。

<key>CLKComplicationPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
<key>CLKComplicationSupportedFamilies</key>
<array>
  <string>CLKComplicationFamilyModularSmall</string>
  <string>CLKComplicationFamilyUtilitarianSmall</string>
  <string>CLKComplicationFamilyUtilitarianSmallFlat</string>
  <string>CLKComplicationFamilyUtilitarianLarge</string>
  <string>CLKComplicationFamilyCircularSmall</string>
  <string>CLKComplicationFamilyExtraLarge</string>
  <string>CLKComplicationFamilyGraphicCorner</string>
  <string>CLKComplicationFamilyGraphicBezel</string>
  <string>CLKComplicationFamilyGraphicCircular</string>
</array>

次に「ComplicationController.swift」を用意します。
これはネットの情報は錯綜(海外情報も)してて、古いバージョンやらなんやらで、結局何が必要なのかイマイチでした。
で、自分で実装したのは以下2のメソッドだけです。
ちなみに、最新のXCODEが作ってくれるテンプレートもイマイチ??で使いませんでした。そもそもWatchOS7でしか利用できないようでしたしね。

  • getLocalizableSampleTemplate(設定するときのプレ表示用)
  • getCurrentTimelineEntry(実際の表示用)

端折ってますがこんな感じです。実際は「getTemplate」の中で、「Info.plist」で設定した分のコンプリケーションテンプレート(CLKComplicationTemplate)を作ってます。
また、キャッシュするデータもこのクラスのメンバ変数として保存します。自分のアプリの場合、場所が変わった場合や、日付が変わった場合をトリガーにしてキャッシュを書き換えています。

class ComplicationController: NSObject, CLKComplicationDataSource {
  ・
  func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
    setTideData(date: Date())
    if let template = getTemplate(complication.family) {
      handler(template)
      return
    }
    handler(nil)
  }
  ・
  func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
    let date = Date()
    setTideData(date: date)
    if let template = getTemplate(complication.family) {
      let entry = CLKComplicationTimelineEntry(
        date: date,complicationTemplate: template)
      handler(entry)
      return
    }
    handler(nil)
  }
  ・
}

次に、各テンプレートが使うアイコンリソースですが、これも必要な分だけ実装します。足りない分は別途ImageSetで追加してもOK。
ちなみに、背景は全て透過で作成します。これを「ComplicationController.swift」の中で各テンプレートに設定します。

s_Assets_xcassets.png

最後に、一定時間で更新する処理を「ExtensionDelegate.swift」に記述して完了です。
追加するのは、「applicationDidEnterBackground」メソッドと、handleメッソッドの中の「case let backgroundTask as WKApplicationRefreshBackgroundTask:」にバックグラウンド処理のスケジュール(scheduleBackgroundRefreshTasks)を入れます。

端折ってますがこんな感じです。

  func applicationDidEnterBackground() {
    // コンプリケーション更新用スケジュール
    scheduleBackgroundRefreshTasks()
  }
  func handle(_ backgroundTasks: Set) {
    // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
    for task in backgroundTasks {
      // Use a switch statement to check the task type
      switch task {
      case let backgroundTask as WKApplicationRefreshBackgroundTask:
        // Schedule the next background update.
        self.scheduleBackgroundRefreshTasks()
        // Be sure to complete the background task once you're done.
        // ↓scheduleBackgroundRefreshTasks を有効にする場合は true に変更
        backgroundTask.setTaskCompletedWithSnapshot(true)
  ・
  ・
  func scheduleBackgroundRefreshTasks() {
    let watchExtension = WKExtension.shared()
    let targetDate = Date().addingTimeInterval(15.0 * 60.0)
    watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { (error) in
      // Check for errors.
      if let error = error {
        print("*** An Background refresh error occurred: \(error.localizedDescription) ***")
        return
      }
      // コンプリケーションデータを更新
      let server = CLKComplicationServer.sharedInstance()
      for complication in server.activeComplications ?? [] {
        server.reloadTimeline(for: complication)
      }
    }
  }

これはどの情報をみても15分に1度みたいだったので、そのタイミングがコンプリケーションのミニマム更新時間みたいです。もちろん、他のアクションから強制更新はできますが、1日に更新できる回数は決まっているみたいです。