😬
[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