[watchOS]WidgetKitになったComplicationsにフルカラーの画像を設定する際に困ったこと

## はじめに ~ take 0 ~

この記事は iOS Advent Calendar 15日目の投稿になります。
毎年普段業務で触れることのない技術に触れる機会として参加しているのですが，
色々試行錯誤した結果，今回 watchOS について触れるのをご容赦ください。

## はじめに

個人開発でも iOS アプリを複数開発していますが，
そのうちのひとつで watchOS アプリの対応することにしました。

アプリ本体の実装を終えて，
最後に文字盤に表示するComplications の実装を行いました。
が，しかしうまく画像が表示できなかったり，
文字盤でユーザが設定したアクセントカラーが適用されているときに
画像が表示されなかったので困りました。

今回は，Complications にフルカラーの画像を表示する画像について書こうと思います。

### watchOS 開発のススメ

アプリのアップデートに際し，
業務だと過去バージョンを切れないなどの制約があったりしますが，
今回はサポートを watchOS 10 以上としてとても楽しく開発できました。
(**Vertical Pagination** マジ好き😍)

みんなあんまり watchOS での開発をしないかもなのですが，
watchOS って使えない API (ビューコンポーネント)もあるものの，
画面も小さく機能を詰め込みすぎることがないので，スッキリ書けて
初めてアプリリリースしようとか開発しようというのにぴったりだと思います。

## Complications とは

![AppIcon](https://docs-assets.developer.apple.com/published/aa12b48e91e69bb604ee505c98fb255d/components-complications-intro~dark@2x.png)
[https://developer.apple.com/design/human-interface-guidelines/complications より](https://developer.apple.com/design/human-interface-guidelines/complications)

Complications は Apple Watch の文字盤で表示可能で，
タイムリーで欲しい情報を確認できるショートカットみたいなものです。

watchOS 9 から WidgetKit として SwiftUI で実装できるようになりました。
WWDC の動画で iPhone のロック画面にも表示されてるのを見て
同じ考え方だなー，なるほどーと思いました。
これ以前は，Complications の種類もたくさんありましたが，
4種類にまとまって動作確認の手間が減ってありがたいです。

## 今回のサンプル

今回のサンプルの仕様は下記のようにします。

* watchOS アプリは実装済みで Complications を新規で追加
* WidgetFamily は4つあるうちの `accessoryCircular` のみ対応してフルカラーの画像を表示させる
* ユーザが設定したアクセントカラーにも対応

![018](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/3c55086d-dcdf-4e87-97ba-419d71a07a5e)

GitHub にコードあげていますので，気になる方はご覧ください。

https://github.com/MilanistaDev/ComplicationsForWidgetKitSample

今回触れませんがアプリ本体は `NavigationSplitView` と
`Vertical Pagination` 使った簡単なサンプルになってます。

### 開発環境

今回の開発環境は下記の通りです。

* Xcode 15.1
* watchOS 10.0 or later

### アプリアイコン

今回のアプリアイコンは下記のものにします。
Keynote でサクッと作りました。
`1024x1024px` でリリース用にアルファチャネルはオフにしています。
Complications 用アイコンは円より外側は切り抜いた画像を `1024x1024px` で用意しました。

| アプリアイコン | Complication アイコン |
| --- | --- |
|![AppIcon](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/fd82216a-7f1d-43a1-89f4-51699c51b8c2)|![WatchComplicationIcon](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/c83082bb-1de0-4791-8ce3-f743005ea0a6 )

## 実装

### Complications 実装用のターゲット追加

File → New → Target... でターゲットを追加します。

![001](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/0665680f-11df-4720-a50f-1cc0633ee987)

Extension ターゲットは watchOS のアプリと同じバンドルIDのプレフィクスを持つ必要あります。
また，実機ビルド・リリースする際にはプロビジョニングプロファイルの生成が必要です。

今回の Complication は画像表示のみでユーザによるカスタマイズを想定していませんので，
**Include Configuration Intent** のチェックは外します。 

![002](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/fe076a4d-65da-4828-a2e7-436cc8819ae9)

スキームは有効にしておきます。

![003](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/0b3da2d1-7a35-4eca-bd4a-be0947e12769)

ターゲットが追加されてファイル群も増えました。

![004](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/e946dfd5-31cf-4e4b-8b71-29d009ea2d04)

これでターゲット追加完了です🙌

### Complication 実装

この状態でアプリをビルドして Widget リストや Complication を設定しようとするとすでにサンプルの表示があります。

|Widget List|Widget Content|
|:--:|:--:|
|<img width="308" alt="005" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/ae80a36a-8bc2-4b31-8812-8b6365e76b17">|<img width="308" alt="006" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/7e77714a-b713-4bf2-b4d7-5570425fb174">|

ターゲット追加時に追加された `WatchComplications.swift` に実装されているものがサンプルとして表示されています。

なのでこのファイルを元に Complication を実装していきます。

最初に Complication 用の画像を追加します。
Complications のターゲットの方の `Assets.xcassets` に追加します。

![007](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/1b0b39ef-7e69-4bc1-8dfa-5743d00b2767)

最後に TimeLine 周りのコードを初期コードから変更していきます。
今回は時間によって表示を変化をさせないので，
下記のように最低限の実装で大丈夫そうです。
`.supportedFamilies([.accessoryCircular])` のモディファイア使って丸い Complications だけをサポートします。


```swift:WatchComplications.swift
import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entries: [SimpleEntry] = [SimpleEntry(date: Date())]
        // No need update timeline
        let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
}

struct WatchComplicationsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Image("WatchComplicationIcon")
            .resizable()
            .scaledToFit()
    }
}

@main
struct WatchComplications: Widget {
    let kind: String = "WatchComplications"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WatchComplicationsEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Complication sample")
        .description("This is an example widget.")
        .supportedFamilies([.accessoryCircular]) // ひとつのみ
    }
}
```

ビルドして Complication の表示を見てみます。
ん？灰色になってる？画像として表示されてない？
タップしたらちゃんとアプリは起動できます。
**これがひとつ目に困ったこと。**

<img width="308" alt="008" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/8d62a6a8-110b-4591-ad50-e022aee5b722">

Xcode 15.0.1 だとちゃんと表示されたりと原因がよくわかってないのですが，
解決した方法としては，画像のサイズをヒューマンインタフェースガイドラインの
指示通りのサイズをApple Watch のサイズごとに用意することでした。

https://developer.apple.com/design/human-interface-guidelines/complications#Circular 

|40mm/42mm|41mm|44mm|45mm/49mm|
|:--:|:--:|:--:|:--:|
|42x42 pt (84x84 px @2x)|44.5x44.5 pt (89x89 px @2x)|47x47 pt (94x94 px @2x)|50x50 pt (100x100 px @2x)|

しかし，Screen Width の設定を individual Widths に変更しても
端末サイズごとに設定できそうにない？足りない？設定が古い？

<img width="959" alt="009" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/45fe4907-6c04-43fe-b5b2-705dee312aef">

なので一番大きいサイズの 100x100px の画像を充てるようにしました。
(`GeometryReader` とか使って端末サイズ取得して使う画像の場合分けが必要っぽい。)
これで無事に表示されました🎉

<img width="308" alt="010" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/c76c1d45-ea6f-4e65-ae32-69372f274888">

### ユーザがアクセントカラーを指定しているときの対応

今まではフルカラー前提で実装してきましたが，
ユーザが文字盤でアクセントカラーを使っているかもしれません。
その際に画像はカラーをマスクしたような見え方になります。

![011](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/18ea9319-a140-46ad-a74e-c02df9271ead)

Widget では `WidgetRenderingMode` として取得可能で 3種類の状態があります。
下記のように `.widgetRenderingMode` キーを使って
環境値からレンダリングモードを読み取ることができ，
各状態で表示させたいビューの実装をできます。

```swift
@Environment(\.widgetRenderingMode) var renderingMode

switch renderingMode {
case .fullColor, .accented, .vibrant
// 各状態で表示させたいビューの実装
```

今回扱うのは，`.accented` の場合になります。

アクセントの色が変わった際に追従させるモディファイアに `.widgetAccentable()` があります。
先ほどの実装で `Image` にこのモディファイアをつけてみて実行してみます。

```swift:WatchComplications.swift
struct WatchComplicationsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Image("WatchComplicationIcon")
           .resizable()
           .scaledToFit()
           .widgetAccentable()
    }
}
```

🤔 
色は追従してるけど画像表示されてない，思ったのと違う・・・
**これがふたつ目に困ったこと。**

![012](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/031f481d-3f20-45c6-aaff-f909f42c6a52)


さてどうしたものか。
結構沼にハマりましたが，結論としてアルファチャネルが関係していました。
アクセントモードだとアルファチャネルの画像が表示されるようです。

> When applying the colors, the system treats the widget’s views as if they were template images. It replaces the view’s color — rendering the new colors while preserving the view’s alpha channel.

レファレンスに書いてあるものの，画像についても同じって書いてほしい🤔

Complication 用の画像も Keynote でサクッと作りましたが
アルファチャネルにまで干渉できない😇ということで，
**GIMP** を使ってアルファチャネルにも画像を用意することにしました。

### アルファチャネルに画像を追加

GIMP で画像を開いて，
`レイヤー→レイヤーマスク→レイヤーマスクの追加`

<img width="514" alt="013" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/3e366ffa-f1e2-4358-b1de-bdb3596a930b">

レイヤーのアルファチャンネルにチェック入れて追加ボタンタップ。

<img width="327" alt="014" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/5819799d-0f85-4313-83f8-f9992116f03e">

アルファチャネルに追加する画像を開く。

<img width="1024" alt="015" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/b45f923b-106d-42ed-b973-ee1cc3a5f241">

追加された画像タップしてを⌘+Aで全て選択してコピー，元の画像をタップしてペースト。
すると透明っぽくなり，アルファ部分も画像が出てきました。

<img width="1024" alt="016" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/5e20c2f2-7baf-4593-943d-a8c24b491572">

この画像を名前をつけてエクスポートしてフルカラー用のものと異なる名前で保存します。

### Complication で作った画像を使い分けできるように

`Assets.xcassets` に追加して2種類にします。

<img width="664" alt="017" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/7c4a00aa-c5a8-4ec9-98e8-ec7ad448e62d">

最後に，設定中のレンダリングモードによって画像の使い分けをするためのコードを実装します。

```swift:WatchComplications.swift
struct WatchComplicationsEntryView : View {
    @Environment(.widgetRenderingMode) var renderingMode
    var entry: Provider.Entry

    var body: some View {
        if renderingMode == .fullColor {
            // フルカラーの画像
            Image("WatchComplicationIcon")
                .resizable()
                .scaledToFit()
        } else {
            // アルファチャネルに画像を用意した画像
            Image("WatchComplicationIcon_accented")
                .resizable()
                .scaledToFit()
                .widgetAccentable() // アクセント色に追従
        }
    }
}
```

実行してアクセントカラーを変えてみると・・・

![018](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/3c55086d-dcdf-4e87-97ba-419d71a07a5e)

うまくいけました🎉
Awesome `(*´∀｀*)`

### SFSymbols とかでいいなら？

Apple 純正のアプリの Complications をみてみると
SFSymbols を使ったシンプルなものも多いです。

この雪のアイコンも SFSymbols で実装するなら下記のような感じ。

```swift:WatchComplications.swift
struct WatchComplicationsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        ZStack {
            AccessoryWidgetBackground()
            
            Image(systemName: "snowflake")
                .resizable()
                .scaledToFit()
                .frame(width: 24.0, height: 24.0)
                .foregroundStyle(.white)
                .widgetAccentable()
        }
    }
}
```

表示は下記のような感じ。これで十分な気もします。

|文字盤表示|アクセントカラー追従|
|:--:|:--:|
|<img width="308" alt="020" src="https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/0dceba65-bee3-4ed2-855a-ebf8152a5f5d">|![019](https://github.com/MilanistaDev/ComplicationsForWidgetKitSample/assets/8732417/4891d87a-cb7a-454d-8111-60a0199f8547)|

わかりやすくて最善な表示を模索したいですね。

## さいごに

WidgetKit になった Complications に
フルカラーの画像を設定する際に困ったことについて書きました。

SwiftUI で実装できるようになり，
コードの共通化・再利用ができそうで良き，良きです。

今回は，アプリショートカットのための Complication で，
TimeLine を使ってタイムリーな情報を表示しませんでしたが，
実装する中で使い方もわかってきたので別の施策で実装してみたいと思います。
iOS/watchOS の Widget の表示サイズ(WidgetFamily)の違いもあり
表示できる情報を精査してユーザに有益なものにしないとですね。

デザインツールとして，Keynote と GIMP 使いましたが，
このあたりはあんまり詳しくないからデザインとツールの勉強もっとしないと。
Vision Pro のアイコンもレイヤー推奨されてたし。

今年もお世話になりました。良いお年をお迎えください。
ご覧いただきありがとうございました。

## 参考

### Complication についての HIG

https://developer.apple.com/design/human-interface-guidelines/complications#Resources 

## WidgetRenderingMode.accented 

https://developer.apple.com/documentation/widgetkit/widgetrenderingmode/accented 

## GIMP

https://www.gimp.org 

## GIMP モノクログレー画像をアルファチャンネルに追加する方法

非常に助かりました。ありがとうございました🙇

https://memonotealpha.net/blog/archives/2869