能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

SwiftUIのColorは条件分岐として指定できるが機能しない罠がある

SwiftUIのColorについての備忘録。Colorは型(文字列や数値)のように扱う事はできないのでSwiftUIでの条件分岐で使用できない(常にfalse)。解決例についても説明する。

1) 概要

SwiftUIのColorはSwiftUI内部でのみ適用される色構造体となっている。色というよりは形状を持たないShapeのようなイメージが近い。Colorの静的変数としてよく使う色がいくつか定義されている(.red, .green, .blue, .cyan)ものを使用するか、豊富に用意されたインスタンス引数を指定できる。インスタンスとして指定可能なのはAssetに定義された色名、rgb指定、hsb指定を指定できる他、UIKitのUIColorやAppKitのNSColorを渡すことも可能となっている。

2) Colorは適用可能なモデファイヤーが豊富

SwiftUIのColorはSwiftUIのシェイプ(Rectangle,RoundedRectangle)などの塗りつぶし色(foregroundColor)として使用できる他、サイズを持たないShapeとしての特性からオーバーレイ指定(overlay)、背景指定(background)両モデファイヤーに指定できる。他色に関する色関連のモデファイヤーは引数としてColorを使用することを覚えておき必要になったら実装例を探すと良い。

3) Color同士は値比較できないがコンパイルが通ってしまう問題

Appleのハードウェアに搭載されているディスプレイにマッチした色表現に変換された結果が見えているため単純な比較をすることはできない場合がある点に注意する。

ダークモードなどと組み合わせるとSwiftUIのColorは単純な値として機能しなくなる可能性があるのでSwiftUI上で@State指定した変数の変化を検知できないなど、表示切り分けの条件分岐として機能しないなどトラブルに遭遇する事がある。

エラーは出ないが比較結果は常にfalseとなる。

追記: iOS14.5の時点でColor周りの改善があったようで挙動は2019年ごろに比べると改善していた。

回避策としては値の比較対処うとしてenumを使用しメソッドでenum値に即したColor返す。enumは条件分岐として機能するのでenumの値に即したColorを返す方法がある。

enum ColorValue {
    case foo1
    case foo2
    case foo3
    case foo4
    func color() -> Color {
        switch( self ){
        case .foo1:
            return Color(UIColor.red)
        case .foo2:
            return Color(UIColor.green)
        case .foo3:
            return Color(UIColor.blue)
        case .foo4:
            return Color(UIColor.cyan)
        }
    }
}

以下にサンプルコードを示す。

import SwiftUI

enum ColorValue {
    case foo1
    case foo2
    case foo3
    case foo4
    func color() -> Color {
        switch( self ){
        case .foo1:
            return Color(UIColor.red)
        case .foo2:
            return Color(UIColor.green)
        case .foo3:
            return Color(UIColor.blue)
        case .foo4:
            return Color(UIColor.cyan)
        }
    }
}

struct ContentView: View {
    @State var colorValue: ColorValue = .foo1
    var body: some View {
        VStack {
            Button(action: {
                colorValue = colorValue == ColorValue.foo1 ? ColorValue.foo2 : ColorValue.foo1
            }){
                Text("ChangeColor")
            }
            if colorValue != .foo1 {
                Rectangle()
                    .frame(width: 100, height: 100)
                    .foregroundColor(colorValue.color())
            } else {
                RoundedRectangle(cornerRadius: 20)
                    .frame(width: 100, height: 100)
                    .foregroundColor(colorValue.color())
            }
        }
    }
}

4) 他の回避策

色を生成する元の値(文字列や、数値、構造体)を比較対象とすることでも対処できる。