5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-12-14

はじめに ~ take 0 ~

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

はじめに

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

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

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

watchOS 開発のススメ

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

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

Complications とは

AppIcon
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

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

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

開発環境

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

  • Xcode 15.1
  • watchOS 10.0 or later

アプリアイコン

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

アプリアイコン Complication アイコン
AppIcon WatchComplicationIcon

実装

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

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

001

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

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

002

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

003

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

004

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

Complication 実装

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

Widget List Widget Content
005 006

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

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

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

007

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

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 の表示を見てみます。
ん?灰色になってる?画像として表示されてない?
タップしたらちゃんとアプリは起動できます。
これがひとつ目に困ったこと。

008

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

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 に変更しても
端末サイズごとに設定できそうにない?足りない?設定が古い?

009

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

010

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

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

011

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

@Environment(\.widgetRenderingMode) var renderingMode

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

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

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

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

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

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

012

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

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 で画像を開いて,
レイヤー→レイヤーマスク→レイヤーマスクの追加

013

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

014

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

015

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

016

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

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

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

017

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

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

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

SFSymbols とかでいいなら?

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

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

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()
        }
    }
}

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

文字盤表示 アクセントカラー追従
020 019

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

さいごに

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

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

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

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

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

参考

Complication についての HIG

WidgetRenderingMode.accented

GIMP

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

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?