【iOS 18】色合い調整でウィジェットが白くなる不具合の対処法

# はじめに
iOS 18では、「色合い調整」という新機能が追加されました。
ホーム画面を長押し → 編集 → カスタマイズ → 色合い調整 の順に操作することで、アイコンやウィジェットの色味を自分好みにカスタマイズできます。

しかし、この「色合い調整」を有効にすると、ウィジェットが白くなり、正しく表示されなくなる不具合が確認されています。

本記事では、この不具合の原因とその対処法について解説します。
# 環境
* Xcode16.2
* iPhone 15 Pro Max（iOS 18.4）
# 不具合の概要
iOS 18の「色合い調整」を有効にすると、ウィジェットが白く塗りつぶされる不具合が発生します。
この影響で、ウィジェット内の画像やテキストなどの要素が白くなり、背景と要素、あるいは要素同士が同化し、表示内容が見えなくなるという問題が生じます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877561/325e7e3a-861f-41ed-8a1e-b37ff639f66f.png" width=30%> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877561/6c850361-17cf-46f9-b721-24b32131204e.png" width=30%>
（左：通常｜右：色合い調整）

## サンプルアプリ
```swift:SampleWidgetView.swift
struct AnimalWidgetView: View {
    @Environment(\.widgetFamily) var family
    var entry: SampleEntry
    
    var body: some View {
        switch family {
        case .accessoryRectangular:
            HStack {
                Image(entry.animal.rawValue)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 30, height: 30)
                Text(entry.animal.displayName)
                    .font(.headline)
            }
            .padding(4)
        case .accessoryCircular:
            ZStack {
                Circle().stroke(Color.orange, lineWidth: 2)
                Image(entry.animal.rawValue)
                    .resizable()
                    .scaledToFit()
                    .padding(6)
            }
        default:
            ZStack {
                Image(entry.animal.rawValue)
                    .resizable()
                    .scaledToFill()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .clipped()
                VStack {
                    Spacer()
                    Text(entry.animal.displayName)
                        .font(.headline)
                        .foregroundColor(Color.red)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 4)
                        .background(Color.black.opacity(0.5))
                        .clipShape(RoundedRectangle(cornerRadius: 6))
                        .padding(.bottom, 12)
                }
            }
        }
    }
}
```
```swift:SampleWidget.swift
struct SampleWidget: Widget {
    let kind: String = "SampleWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: SampleWidgetProvider()) { entry in
            if #available(iOS 17.0, *) {
                AnimalWidgetView(entry: entry)
                    .containerBackground(Color.sampleBGColor, for: .widget)
            } else {
                AnimalWidgetView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("干支時計ウィジェット")
        .description("今の刻に応じた動物を表示します。")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}
```
# 原因
「色合い調整」を有効にすると、 [widgetRenderingMode](https://developer.apple.com/documentation/widgetkit/widgetrenderingmode) が [fullColor](https://developer.apple.com/documentation/widgetkit/widgetrenderingmode/fullcolor) から [accented](https://developer.apple.com/documentation/widgetkit/widgetrenderingmode/accented) に変更されます。
ウィジェットの`widgetRenderingMode`が`accented`に切り替わると `Image()`、`Text()`などの要素は、システムによって以下の2つのグループに分類されます。

デフォルト：アルファチャンネル（白色）で表示される
アクセント：ユーザーが選択した色でアルファチャンネルに色が付けられて表示される

今回の不具合では、ウィジェットが白く塗りつぶされていることから、すべての要素がデフォルトグループとして処理されていると考えられます。
さらに、`.background()`を指定しているViewも同様に白く塗りつぶされてしまいます。

:::note info
アルファチャンネルとは
画像データなどの色情報の一部として、その画素の透過度(背後の色が透き通る度合い)を設定できるようにした領域。
— 引用元：[e-words「アルファチャンネル」](https://e-words.jp/w/%E3%82%A2%E3%83%AB%E3%83%95%E3%82%A1%E3%83%81%E3%83%A3%E3%83%B3%E3%83%8D%E3%83%AB.html)
:::
# 対処法
## ❌ 効果がなかった対処法
要素がデフォルトグループに分類されていると考え、親View（`ZStack`）に[.widgetAccentable(true)](https://developer.apple.com/documentation/swiftui/view/widgetaccentable(_:))を適用してアクセントグループとして扱われるようにしました。
しかし、この対応では不具合を解消することはできませんでした。
```swift:SampleWidgetView.swift
default:
    ZStack {
        Image(entry.animal.rawValue)
        .resizable()
        .renderingMode(.original)
        .scaledToFill()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .clipped()
        VStack {
            Spacer()
            Text(entry.animal.displayName)
                .font(.headline)
                .foregroundColor(Color.red)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(Color.black.opacity(0.5))
                .clipShape(RoundedRectangle(cornerRadius: 6))
                .padding(.bottom, 12)
        }
    }
    .widgetAccentable(true)
}
```
`.widgetAccentable(true)`適用後のウィジェット
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877561/6f390bb2-3db9-4afa-9cfe-6d46d7c82da5.png" width=30%> 
## 🟢 有効だった対処法
`body`プロパティに`.containerBackgroundRemovable(false)`を追加します。
```swift:SampleWidget.swift
struct SampleWidget: Widget {
    let kind: String = "SampleWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: SampleWidgetProvider()) { entry in
            if #available(iOS 17.0, *) {
                AnimalWidgetView(entry: entry)
                    .containerBackground(Color.sampleBGColor, for: .widget)
            } else {
                AnimalWidgetView(entry: entry)
                    .padding()
                    .background(Color.sampleBGColor)
            }
        }
        .configurationDisplayName("干支時計ウィジェット")
        .description("今の刻に応じた動物を表示します。")
        .supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular, .accessoryRectangular])
        // 追加
        .containerBackgroundRemovable(false)
    }
}
```
修正後のウィジェット
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877561/d289b222-94e4-42b2-b2e6-041fd934cb06.png" width=30%> 

### なぜ修正できたのか
`.containerBackgroundRemovable(false)`を追加することで、ウィジェットの背景がシステムにより自動で置き換えられるのを防げます。
これにより、「色合い調整」による背景の置き換えが発生しなくなります。

また、`.containerBackground()`はウィジェット全体（親View）に適用されているため、`VStack`や`Text`、`Image`などの子Viewに対しても同様の挙動が反映されていると考えられます。

その結果、システムが背景の取り外しに関与できなくなったため、本来の背景やUI要素がそのまま描画され、不具合が発生しなくなったと考えられます。
### デグレとその対応
ロック画面ウィジェットで背景が表示されるというデグレが発生しました。

**原因**
不具合の対処で`.containerBackgroundRemovable(false)`を追加したことにより、`containerBackground`がシステムによって取り外されなくなったためです。

**対応**
このデグレについては、ロック画面ウィジェットに`.containerBackground(Color.clear, for: .widget)`を適用することで対応しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877561/cc79a6d9-028f-4567-9520-882acdf4018b.png" width=30%> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877561/1308cd84-406c-4792-bca0-7eb8b8682774.png" width=30%>
（左：デグレ発生時｜右：デグレ対応後）
# おわりに
本記事で紹介した`.containerBackgroundRemovable(false)`による対応が、同様の不具合に悩んでいる方の助けになれば幸いです。

アクセシビリティ関連の新機能は意外な落とし穴になりがちなので、今後も注意深く見ていこうと思います。