😬
[SwiftUI] Color + AppStorage + Pickerで苦戦した話
TL, DR
やりたかったこと
- SwiftUIの
ColorをPickerで選択できる画面を作成し、選択した結果はAppStorageに保存する。
難しい点
-
ColorはAppStorageに保存できる型ではない。 -
ColorをRawRepresentableでStringに変換するとAppStorageに保存できるようになるが、Pickerが機能しなくなる。
解決策(2案)
-
ColorをRawRepresentableにしてAppStorageに保存できるようにし、ColorPickerを使う - 自作で
enumを定義し、Colorと対応させるようにし、Pickerを使う
やりたかったこと
- SwiftUIの
ColorをPickerで選択できる画面を作成し、選択した結果は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に変換できるようにする。
StringはAppStorageに保存できる型なので、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 ""
}
}
}
これで、上のエラーは出なくなり、ColorをAppStorageに保存できる。
難しい点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.redをdumpしてみた。
▿ red
▿ provider: SwiftUI.(unknown context at $10ac2f438).ColorBox<SwiftUI.SystemColorType> #0
- super: SwiftUI.AnyColorBox
- super: SwiftUI.AnyShapeStyleBox
- base: SwiftUI.SystemColorType.red
- Code0の
colorをdumpしてみた。-
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の
colorをdumpしてみた。-
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の
colorをdumpしてみた。- これは、自分で定義した
enumの型になっている。
- これは、自分で定義した
- ColorPickerDemo.ColorItem.red
Pickerはtagのドキュメントにあるように、
The Picker requires the tags to have a type that exactly matches the selection type
$selectionとtag()の型が厳密に一致している必要があるらしい。
Code2が思った通りに動作しないのは、上記のdumpした結果にあるように、Code2でのtagの型が$selectionとして与えたColor型と厳密に一致しなかったから?
そして、ColorPickerはそのあたり、許容するように設計されている?
Discussion