SwiftUI 使って Watch Complication を実装してみる!

## はじめに

Complication を SwiftUI で作れるようになったと聞いて，
WWDC のころは一人で歓喜していたのだが，結局触らずじまい😩
どうしたら SwiftUI 使って Complication 設定できるんだーと
少し困ったので調べてサンプル実装してみました。

Watch 周りの情報本当に少ない・・・

## Complications とは

Apple Watch 持っている方はご存知の通り，
タイムリーな情報が Watch Face に表示され，Watch を見るだけで幸せになるものです。
純正では，Activity や 天気情報，バッテリ残量など色々ありますね。

すでに watchOS 7，Series 6 が出ていることもあり，かなり種類も増えてきました。

どんな種類があるかなどは過去に記事書いてるのでご覧ください。
といいつつ古くなってるし Human Interface Guideline(以下 HIG)や
公式の情報を確認した方が良いかもです。

**Apple Watch Series 4 の新しい Complication Families の実装を試してみる**
[https://qiita.com/MilanistaDev/items/6a09bb787d9eee509c0d](https://qiita.com/MilanistaDev/items/6a09bb787d9eee509c0d)


## Complications を SwiftUI 使う方法

色々(無駄に)調べた結果，
`ComplicationController` に SwiftUI を import するだけでした💡

`CLKTextProvider` や `CLKGaugeProvider` を
それぞれの Template に設定して表示するのですが，
いくつかの Template で SwiftUI の単一の View だけを使えるものがあります。

* **CLKComplicationTemplateGraphicCornerCircularView**
* **CLKComplicationTemplateGraphicCircularView**
* **CLKComplicationTemplateGraphicRectangularFullView**
* **CLKComplicationTemplateGraphicExtraLargeCircularView**

今回は `CLKComplicationTemplateGraphicRectangularFullView` を使ってみたいと思います。

`CLKTextProvider` や `CLKGaugeProvider`，Template については，
Xcode から `CLKComplicationTemplate.h` や HIG の Complications の項目をご覧ください。

[https://developer.apple.com/design/human-interface-guidelines/watchos/overview/complications/](https://developer.apple.com/design/human-interface-guidelines/watchos/overview/complications/)

## 今回やること

今回は，`GraphicRectangular` 部分を
SwiftUI で View を作って表示させてみます。

題材は，**東京メトロ東西線の行き先表示器** 風にしました。
データの更新は今回は扱いません。(運行はフィクション，妄想です)
朝の 8時台に九段下ゆきの運行が平日一本あるのは事実です(レア)。

![スクリーンショット 2020-12-23 8.18.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/e10b9f1f-5e87-1c7d-e86b-0fc551ff76c1.png)

今回のコードは，Github に Push したので気になる方はご覧ください。

[https://github.com/MilanistaDev/WatchComplicationsForSwiftUI](https://github.com/MilanistaDev/WatchComplicationsForSwiftUI)


### 実装環境

* Xcode 12.2
* watchOS 7.0 and later

iOS アプリは不要のため，Watch App 単体にしています。
![スクリーンショット 2020-12-23 22.13.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/8cafbb59-18e1-f78f-d6b1-c453e3536d6e.png)


## 実装

### Complication に表示させる View の実装

Watch App のファーストビューを SwiftUI で実装し，
その画面を Complication にも表示させてみます。
コードについては `VStack` と `HStack` を使っていくだけなので説明は省略します。

<details><summary>コード</summary><div>

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            ForEach(0..<sampleData.count) { index in
                TimeTableView(operationSchedule: sampleData[index])
            }
        }
        .padding()
        .background(Color.noticeBg)
    }
}

struct TimeTableView: View {
    
    let operationSchedule: OperationSchedule!
    
    var body: some View {
        HStack {
            Text(operationSchedule.arrivalTime)
                .font(.subheadline)
                .bold()
            Text(operationSchedule.trainType.kind.name)
                .font(.footnote)
                .bold()
                .frame(width: 40.0)
                .foregroundColor(.white)
                .background(operationSchedule.trainType.kind.color)
            Image(operationSchedule.finalStaIcon)
                .resizable()
                .frame(width: 20.0, height: 20.0)
            Spacer()
            Text(operationSchedule.finalStaName)
                .font(.subheadline)
                .bold()
            Spacer()
        }
    }
}

struct OperationSchedule {
    var arrivalTime: String
    var trainType: TrainType
    var finalStaIcon: String
    var finalStaName: String
}

enum TrainType {
    case rapid
    case rapidCommuting    // 通勤快速
    case local             // 各駅停車
    
    struct Kind {
        var name: String
        var color: Color
    }
    
    var kind: Kind {
        switch self {
        case .rapid:
            return Kind(name: "快速", color: .red)
        case .rapidCommuting:
            return Kind(name: "通快", color: .green)
        case .local:
            return Kind(name: "各停", color: .blue)
        }
    }
}

let sampleData: [OperationSchedule] = [
    OperationSchedule(arrivalTime: "8:20", trainType: .local,
                      finalStaIcon: "T07", finalStaName: "九段下"),
    OperationSchedule(arrivalTime: "8:24", trainType: .rapidCommuting,
                      finalStaIcon: "T01", finalStaName: "中野"),
    OperationSchedule(arrivalTime: "8:28", trainType: .rapid,
                      finalStaIcon: "JB01", finalStaName: "三鷹")
]

extension Color {
    static let noticeBg = Color(UIColor(named: "noticeBg")!)
}
```

</div></details>

![スクリーンショット 2020-12-23 22.42.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/7e40c5c7-0f13-f9dd-477d-7f28efbd3bfd.png)


### Complication 表示に向けての実装

Xcode 12 で新規に Watch App のプロジェクトを作ると，すでに
`ComplicationController.swift` があるのでこのクラスで設定を行います。
既存のアプリで設定をする場合は，`CLKComplicationDataSource` に
準拠させたクラスを作ることになります。

今回は，`CLKComplicationTemplateGraphicRectangularFullView` を使ってみたいので Complication の Family の `GraphicRectangular` を使えるようにします。
初期値は全ての Family が設定されている(enum の allCases)ので書き換えます。

```swift:ComplicationController.swift
func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
    let descriptors = [
        CLKComplicationDescriptor(identifier: "complication", displayName: "WatchComplicationsForSwiftUI", supportedFamilies: [CLKComplicationFamily.graphicRectangular])
        // Multiple complication support can be added here with more descriptors
    ]
    // Call the handler with the currently supported complication descriptors
    handler(descriptors)
}
```


次に，Complication 表示のために `getCurrentTimelineEntry` 関数部分に
`CLKComplicationTimelineEntry` の設定をします。
`date` は表示する時間，`complicationTemplate` は表示させるテンプレートです。

```swift:ComplicationController.swift
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
    // Call the handler with the current timeline entry
    if let template = getComplicationTemplate(for: complication, using: Date()) {
        let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        handler(entry)
    } else {
        handler(nil)
    }
}
```

テンプレートの作成は別関数で行うことが多いので関数を分けています。
SwiftUI を import することで `CLKComplicationTemplateGraphicRectangularFullView` を使えるようになります。
これだけでなく，SwiftUI が利用可能な Template は全て使えるようになります。

```swift:ComplicationController.swift
import ClockKit
import SwiftUI // 追加

// 省略

func getComplicationTemplate(for complication: CLKComplication, using date: Date) -> CLKComplicationTemplate? {
    switch complication.family {
    case .graphicRectangular:
        return CLKComplicationTemplateGraphicRectangularFullView(ContentView())
    default:
        return nil
    }
}
```

これで表示させる準備が整いました。


### Apple Watch 本体で Complication を設定

プロジェクトを Run してアプリを一旦起動して，
Watch Face 画面に戻ります。
画面をロングタップして `GraphicRectangularFullView` が利用可能な
**Infograph Modular** や **Modular Compact** の Face にします。

|Infograph Modular|Modular Compact|
|:--:|:--:|
|![スクリーンショット 2020-12-23 21.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/844bd725-cfda-0a90-c273-abfd0d415810.png)|![スクリーンショット 2020-12-23 21.46.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/97d25b32-08cd-abe3-eaf1-b17b5f3bf031.png)  |

Complication を設定する画面から選択します。

|左右にスワイプ|選択してセット|
|:--:|:--:|
|![スクリーンショット 2020-12-23 21.49.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/76943086-5199-5ba9-656d-d8b5977f6ccd.png)|![スクリーンショット 2020-12-23 8.18.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/d988acea-a754-108f-c6b9-c4dad271e42c.png)|

`ComplicationController` の `getLocalizableSampleTemplate` 関数で
Template を設定することで選択時にサンプルが見えるようになります。
実際にアプリをリリースした際はぜひ実装しておきたい部分です。

```swift:ComplicationController.swift
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
    // This method will be called once per supported complication, and the results will be cached
    let template = getComplicationTemplate(for: complication, using: Date())
    handler(template)
}
```

実装すると，選択時にサンプルビューが表示されています。

![スクリーンショット 2020-12-23 23.32.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/f3fd3899-6bd7-0218-33cc-2e7148135cbf.png)

設定後に Watch Face に戻ってくると・・・
表示されました🎉

![スクリーンショット 2020-12-23 8.18.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/45e61186-0130-dccb-80d1-c02a3ccbe8f7.png)


### Complication のプレビュー

SwiftUI で View を実装する際にすごく役立つのが，
Canvas にプレビューされる機能です。
Complication もどう表示されるか確認したいのですが・・・
できます👍

例えば，`ComplicationController.swift` のファイルの最後に
`PreviewProvider` を追加し下記のように実装します。

```swift:ComplicationController.swift

struct ComplicationController_Previews: PreviewProvider {
    static var previews: some View {
        CLKComplicationTemplateGraphicRectangularFullView(ContentView()).previewContext()
    }
}
```

`previewContext()` に引数を取らない場合は，
デフォルトでフルカラーの Watch Face になります。 
![スクリーンショット 2020-12-23 23.01.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88266/b0484f31-8adb-fead-919b-449db2ab9dd3.png)

全部の Watch Face tinting のプレビューを確認する場合は下記のようにします。

```swift:ComplicationController.swift
struct ComplicationController_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(CLKComplicationTemplate.PreviewFaceColor.allColors) { color in
            CLKComplicationTemplateGraphicRectangularFullView(ContentView()).previewContext(faceColor: color)
        }
    }
}
```

下記のように，設定可能なカラーのプレビューが一気に確認できます。
![preview](https://user-images.githubusercontent.com/8732417/103015427-c1d62000-4583-11eb-803d-c37e2cd6601b.gif)


#### SafeArea を無視する

Complication の領域は，
40mm の Apple Watch の場合は 162 × 69pt，
44mm の Apple Watch の場合は 184 × 78pt になりますが，
SafeArea を無視することもできます。

お馴染みの `edgesIgnoringSafeArea(_:)` を使います。

今回の View だと問題なかったですが，見切れたりする場合に設定します。

#### 注意点

SwiftUI で View を実装できるからといって，
`tapGesture` や `Button` などを使ったアクションは使えないです。
Complication のタップはあくまでも Watch App の起動になるためです。

また，アニメーションなども使えないです。

## Future Work

`Gauge` や `Progress View` なども重要な要素ですし，扱ってみたいですね。

また今回はあまり触れなかったのですが，
Watch Face Tinting(Watch Face の色) を考慮した，
Complication の設計についても一度やってみたいです。

セッション動画の最後にベストプラクティスの項目があるのですが，
今回紹介できずでした。しっかり確認したいと思います。

## おわりに

今回は，SwiftUI 使って Watch Complication を実装してみました。

パズルのようにレイアウトを組んでいた画面も SwiftUI のおかげで改善され，
watchOS 7 からは Complication も SwiftUI で自由に実装できる
Template が登場しました。これからも楽しみなガジェットには違いないですね！

今年はコロナ禍もあり，公私共に積極的に活動できなかったです。
もっと目標を明確にして活動していこうと思います。

ご覧いただきありがとうございました。
良いお年をお迎えください🎍

## 参考

**Building Complications with SwiftUI**
[https://developer.apple.com/documentation/clockkit/building_complications_with_swiftui](https://developer.apple.com/documentation/clockkit/building_complications_with_swiftui)

**Build complications in SwiftUI (WWDC20セッション動画)**
[https://developer.apple.com/videos/play/wwdc2020/10048/](https://developer.apple.com/videos/play/wwdc2020/10048/)