😬

[SwiftUI] Color + AppStorage + Pickerで苦戦した話

2024/04/18に公開

TL, DR

やりたかったこと

  • SwiftUIのColorPickerで選択できる画面を作成し、選択した結果はAppStorageに保存する。

難しい点

  • ColorAppStorageに保存できる型ではない。
  • ColorRawRepresentableStringに変換するとAppStorageに保存できるようになるが、Pickerが機能しなくなる。

解決策(2案)

  1. ColorRawRepresentableにしてAppStorageに保存できるようにし、ColorPickerを使う
  2. 自作でenumを定義し、Colorと対応させるようにし、Pickerを使う

やりたかったこと

  • SwiftUIのColorPickerで選択できる画面を作成し、選択した結果はAppStorageに保存する。
Code0.swift
struct ColorPickerView: View {
    @AppStorage("color") var color: Color = .red
    var body: some View {
        HStack {
            Text("PickerView")
            Picker("", selection: $color) {
                Text("Red").tag(Color.red)
                Text("Blue").tag(Color.blue)
                Text("Green").tag(Color.Green)
            }
            Spacer()
            Circle().fill(color).frame(width: 30)
        }
    }
}

難しい点1

問題

Color@AppStorageに保存できる型ではない。
上のコードは、以下のエラーを吐く。

No exact machted in call to initializer

対策

Colorのextensionを書いて、RawRepresentableにしてStringに変換できるようにする。
StringAppStorageに保存できる型なので、Colorも保存できるようになる。

Code1.swift
extension Color: RawRepresentable {
    public init?(rawValue: String) {
        guard let data = Data(base64Encoded: rawValue) else {
            self = .black
            return
        }
        do {
            let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) ?? .black
            self = Color(color)
        } catch {
            self = .black
        }
    }
    
    public var rawValue: String {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: UIColor(self), requiringSecureCoding: false) as Data
            return data.base64EncodedString()
        } catch {
            return ""
        }
    }
}

これで、上のエラーは出なくなり、ColorAppStorageに保存できる。

難しい点2

問題

上の変更を加えると、今度はPickerが思ったように機能しなくなる。
Pickerで選択をすると、Circleの色は変わるし、アプリを起動しなおしても、前回の選択が保存されている。
しかし、Pickerのテキストが変化しなくなっている。(e.g blueを選んでも、redのまま)

対策1

Pickerの代わりに、ColorPickerを使う。

Code2.swift
extension Color: RawRepresentable {
    ... Code1 ...
}

struct ColorPickerView: View {
    @AppStorage("color") var color: Color = .red
    var body: some View {
        HStack {
            ColorPicker("PickerView", selection: $color)
            Text("Selected color ->")
            Circle().fill(color).frame(width: 30, height: 30)
        }
    }
}

対策2

自作でenumを定義し、Colorと対応させるようにし、Pickerを使う

Code3.swift
enum ColorItem: String { // AppStorageに保存するために、Stringにする
    case red
    case blue
    case green
    
    var color: Color {
        switch self {
            case .red: return Color.red
            case .blue: return Color.blue
            case .green: return Color.green
        }
    }
}

struct ColorPickerView: View {
    @State var color: ColorItem = .red
    var body: some View {
        HStack {
            Text("PickerView")
            Picker("", selection: $color) {
                Text("Red").tag(ColorItem.red)
                Text("Blue").tag(ColorItem.blue)
                Text("Green").tag(ColorItem.green)
            }
            
            Spacer()
            Circle().fill(color.color).frame(width: 30)
        }
    }
}

考察

RawRepresentableで得たColor型は厳密には、元のColor.redなどとは型が異なっていた?
以下は、いろいろdumpしてみた結果である。

  • Color.reddumpしてみた。
▿ red
    ▿ provider: SwiftUI.(unknown context at $10ac2f438).ColorBox<SwiftUI.SystemColorType> #0
    - super: SwiftUI.AnyColorBox
        - super: SwiftUI.AnyShapeStyleBox
    - base: SwiftUI.SystemColorType.red
  • Code0のcolordumpしてみた。
    • ColorPickerで選んだColorは、UIDynamicCatalogSystemColor?
 <UIDynamicCatalogSystemColor: 0x600001776080; name = systemRedColor>
  ▿ provider: SwiftUI.(unknown context at $10ab77438).ColorBox<__C.UIColor> #0
    - super: SwiftUI.AnyColorBox
      - super: SwiftUI.AnyShapeStyleBox
    - base: <UIDynamicCatalogSystemColor: 0x600001776080; name = systemRedColor> #1
      - super: UIDynamicColor
        - super: UIColor
          - super: NSObject
  • Code2のcolordumpしてみた。
    • ColorPickerで選んだColorは、UIExtendedSRGBColorSpace?
▿ UIExtendedSRGBColorSpace 1.00044 0.227608 0.186702 1
  ▿ provider: SwiftUI.(unknown context at $10849b438).ColorBox<__C.UIColor> #0
    - super: SwiftUI.AnyColorBox
      - super: SwiftUI.AnyShapeStyleBox
    - base: UIExtendedSRGBColorSpace 1.00044 0.227608 0.186702 1 #1
      - super: UIColor
        - super: NSObject
  • Code3のcolordumpしてみた。
    • これは、自分で定義したenumの型になっている。
- ColorPickerDemo.ColorItem.red

Pickertagのドキュメントにあるように、

The Picker requires the tags to have a type that exactly matches the selection type

$selectiontag()の型が厳密に一致している必要があるらしい。
Code2が思った通りに動作しないのは、上記のdumpした結果にあるように、Code2でのtagの型が$selectionとして与えたColor型と厳密に一致しなかったから?
そして、ColorPickerはそのあたり、許容するように設計されている?

References

Discussion