SwiftUI の Button(role: .destructive) は List 内で SF Symbol を赤くしない

AI 主体で執筆

設定画面で「削除」や「リセット」のような破壊的ボタンを作ったとき、テキストは赤いのにアイコンだけ青い——という経験はありませんか。symbolRenderingMode を変えても、シンボルを変えても直らず、最終的に原因と Apple の設計パターンに気づいた記録です。

現象:テキストは赤、アイコンは青

SwiftUI の List 内で Button(role: .destructive)Label を組み合わせると、テキストには .destructive の赤色が適用されますが、SF Symbol のアイコンにはアクセントカラー(青)が使われたままになります。

Button(role: .destructive) {
    // action
} label: {
    Label("Drive から再取得",
          systemImage: "exclamationmark.arrow.trianglehead.counterclockwise.rotate.90")
}

実行すると、こうなります:

期待した見た目
Drive から再取得
実際の見た目
Drive から再取得

試したこと(すべて効果なし)

1. symbolRenderingMode(.monochrome)

階層的なレンダリングが原因かと思い、モノクロを強制しましたが変わりません。モノクロにはなるものの、色はアクセントカラーのままです。

Label("Drive から再取得", systemImage: "...")
    .symbolRenderingMode(.monochrome)

2. シンプルなシンボルに変更

arrow.counterclockwise のようなデフォルトでモノクロのシンボルに変えても同じです。シンボルの種類に関係なく、List 内の destructive ボタンではアイコンが青のままでした。

Label("Drive から再取得", systemImage: "arrow.counterclockwise")

結論: これはシンボルのレンダリングモードの問題ではなく、Button(role: .destructive)List 内で SF Symbol の色を変更しないという SwiftUI の挙動そのものです。

対処法

方法 A:.foregroundStyle(.red) を明示する

Label に直接 .foregroundStyle(.red) を指定すれば、アイコンとテキストの両方が赤になります。

Button(role: .destructive) {
    // action
} label: {
    Label("Drive から再取得", systemImage: "arrow.counterclockwise")
        .foregroundStyle(.red)
}

role: .destructive はアクセシビリティ(VoiceOver で破壊的操作と伝える)のために残しつつ、色は明示的に指定します。

注意点として、.foregroundStyle(.red) を指定すると .disabled(true) 時の自動グレーアウトが効かなくなる可能性があります。.opacity で明示的に制御しているなら問題ありません。

方法 B:アイコンを使わない

Apple 純正の設定アプリでは、破壊的操作のボタンはほぼテキストのみです。

Apple 自身がこの問題を回避しているとも言えますし、破壊的操作を視覚的に区別するための意図的なデザイン選択とも解釈できます。

どちらを選ぶべきか

画面内の他のボタンとの一貫性で判断するのが良いです。

今回の 60d Memo では、設定画面の独立行ボタンがすべてアイコン付きだったため、方法 A を採用しました。1箇所だけアイコンがないと、かえって不自然だったためです。

Button(role: .destructive) {
    showRebuildFromDriveConfirm = true
} label: {
    // .destructive role does not tint SF Symbols in List rows
    Label(String(localized: "Re-download from Drive"),
          systemImage: "arrow.counterclockwise")
        .foregroundStyle(.red)
}
.disabled(syncing)
.opacity(syncing ? 0.4 : 1)

まとめ