[SwiftUI] SwiftUI の Color を UserDefaults に保存する

SwiftUI

SwiftUI の Color を UserDefaults に保存する方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.1
  • Xcode 12.3
  • iOS 14.2
  • SwiftyUserDefaults

Color を UserDefaults に保存

NSColor や UIColor もありますが、SwiftUI で使うのは、Color です。

UserDefaults は、アプリのちょっとした情報を保存することができ非常に便利ですが、保存できるタイプが制限されています。

Color は、保存できるタイプではありません。(UIColor, NSColor もサポート対象ではありません。)

この記事では、Color を UserDefaults に保存する方法を説明します。

SwiftyUserDefaults

UserDefaults を扱う時に便利な SwiftyUserDefaults を使います。

SwiftyUserDefaults は、こちら

DefaultsKEys の extension を定義することで、Defaultas[キー] のような形でのアクセスが可能となるライブラリです。

前準備

Color の情報は、RGB 情報で再構成できるはずなので、UserDefaults には、RGB 情報を保存しておくことにします。
しかし、問題がひとつあります。Color を RGB 値から作成することはできるのですが、Color から RGB 値を取得することはできません。

ですので、以下のような extension を作り、RGB(A) 値を取得できるようにしておきます。

Color#rgbValues

#if os(iOS)
typealias SystemColor = UIColor
#elseif os(macOS)
typealias SystemColor = NSColor
#else
#error("your os is not supported")
#endif

extension SystemColor {
    var rgba: (red: Double, green: Double, blue: Double, alpha: Double) {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (Double(red), Double(green), Double(blue), Double(alpha))
    }
}

extension Color {
    var rgbValues:(red: Double, green: Double, blue: Double){
        let rgba = SystemColor(self).rgba
        return (rgba.red, rgba.green, rgba.blue)
    }
}

# iOS と macOS のどちらからも使えるように、UIColor/NSColor を typealias で別名定義して使用しています

Color を保存

2つの方法があります。少し方法が異なりますが、どちらも RGB 値を保存して、RGB 値から Color を再現するところは同じです。

直接 RGB 値を保存

1つの Color に対して、R/G/B それぞれの値を保存する用のキーを作成します。

DefaultsKeys 定義

extension DefaultsKeys {
    // for Color1
    var color1R:DefaultsKey { .init("Color1R", defaultValue: 0) }
    var color1G:DefaultsKey { .init("Color1G", defaultValue: 0) }
    var color1B:DefaultsKey { .init("Color1B", defaultValue: 0) }
}
実装

    // 読み込み箇所
    init() {
        color1 = Color(red: Defaults[\.color1R], green: Defaults[\.color1G], blue: Defaults[\.color1B])
    }
    // 保存箇所
    didSet {
        let rgb = color1.rgbValues
        Defaults[\.color1R] = rgb.red
        Defaults[\.color1G] = rgb.green
        Defaults[\.color1B] = rgb.blue
    }

Color を Serializable にして保存

Color を Codable に準拠させるような extension を用意して、Serializable な型とすることで、encode/decode の仕組みを使って保存します。

Color を Codable に

extension Color:Codable {
    enum CodingKeys: String, CodingKey {
        case red
        case green
        case blue
    }
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let red = try values.decode(Double.self, forKey: .red)
        let green = try values.decode(Double.self, forKey: .green)
        let blue = try values.decode(Double.self, forKey: .blue)
        self.init(red: red, green: green, blue: blue)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let rgba = self.rgbValues
        try container.encode(rgba.red, forKey: .red)
        try container.encode(rgba.green, forKey: .green)
        try container.encode(rgba.blue, forKey: .blue)
    }
}

extension Color:DefaultsSerializable {}
DefaultsKeys 定義

extension DefaultsKeys {
    // for Color2
    var color2:DefaultsKey { .init("Color2", defaultValue: Color.white)}
}
実装

    // 読み込み箇所
    init() {
        color2 = Defaults[\.color2]
    }
    // 保存箇所
    didSet {
        Defaults[\.color2] = self.color2
    }

使い分け?

1つ目の方法は、自分で Color を分解して保存、読み込んで組み立てることをしています。

2つ目の方法は、Color を Codable に準拠させ、あとは、元からある仕組みを使って保存する方法です。

保存対象の Color が1つしかないのであれば、大きな差はありませんが、複数の Color を扱う必要があるのであれば、2つ目の方法が便利になりそうです。

テストに使ったコード

以下のコードを使ってテストしています。非常にシンプルなコードですが、参考までに貼っておきます。

example

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/01/16
//  © 2021  SmallDeskSoftware
//

import SwiftUI
import SwiftyUserDefaults

#if os(iOS)
typealias SystemColor = UIColor
#elseif os(macOS)
typealias SystemColor = NSColor
#else
#error("os is not supported")
#endif

extension SystemColor {
    var rgba: (red: Double, green: Double, blue: Double, alpha: Double) {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (Double(red), Double(green), Double(blue), Double(alpha))
    }
}

extension Color {
    var rgbValues:(red: Double, green: Double, blue: Double){
        let rgba = SystemColor(self).rgba
        return (rgba.red, rgba.green, rgba.blue)
    }
}

extension Color:Codable {
    enum CodingKeys: String, CodingKey {
        case red
        case green
        case blue
    }
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let red = try values.decode(Double.self, forKey: .red)
        let green = try values.decode(Double.self, forKey: .green)
        let blue = try values.decode(Double.self, forKey: .blue)
        self.init(red: red, green: green, blue: blue)
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let rgba = self.rgbValues
        try container.encode(rgba.red, forKey: .red)
        try container.encode(rgba.green, forKey: .green)
        try container.encode(rgba.blue, forKey: .blue)
    }
}

extension Color:DefaultsSerializable {}


class ColorModel {
    var color1: Color {
        didSet {
            let rgb = color1.rgbValues
            Defaults[\.color1R] = rgb.red
            Defaults[\.color1G] = rgb.green
            Defaults[\.color1B] = rgb.blue
        }
    }
    var color2: Color = Color.white {
        didSet {
            Defaults[\.color2] = self.color2
        }
    }
    init() {
        color1 = Color(red: Defaults[\.color1R], green: Defaults[\.color1G], blue: Defaults[\.color1B])
        color2 = Defaults[\.color2]
    }
}

class ViewModel: ObservableObject {
    @Published var colors: ColorModel

    public init() {
        colors = ColorModel()
    }
}

struct ContentView: View {
    @StateObject private var colorModel: ViewModel = ViewModel()
    
    var body: some View {
        VStack {
            GroupBox(label: Text("SavedColor")) {
                Text("Color1 is \(colorModel.colors.color1.description)")
                    .padding()
                ColorPicker("Color1", selection: $colorModel.colors.color1)
            }
            GroupBox {
                Text("Color2 is \(colorModel.colors.color2.description)")
                    .padding()
                ColorPicker("Color2", selection: $colorModel.colors.color2)
            }
                
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

extension DefaultsKeys {
    // for Color1
    var color1R:DefaultsKey { .init("Color1R", defaultValue: 0) }
    var color1G:DefaultsKey { .init("Color1G", defaultValue: 0) }
    var color1B:DefaultsKey { .init("Color1B", defaultValue: 0) }
    
    // for Color2
    var color2:DefaultsKey { .init("Color2", defaultValue: Color.white)}
}

まとめ:Color を UserDefaults に保存する

Color を UserDefaults に保存する
  • RGB 値それぞれを個別に保存する
  • Color を Codable 準拠にして保存する
  • (補足) Color から RGB 値を取得するときは、UIColor/NSColor を使うと便利

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です