iPadawan
iPadawan

Reputation: 1110

Saving Color to UserDefaults and use it from @AppStorage

In my App for macOS and iOS I use colors created from here: https://uiwjs.github.io/ui-color/ and then f.e. Works fine.

Color(red: 1.47, green: 1.9, blue: 2.3).opacity(1)

However, for some colors I want them saved in UserDefaults and read/write by UserDefaults.standard methods and read/write by @AppStorage.

I did try to use, but this gives me runtime errors.

static let infoListRowReadBGColor = Color(red: 2.55, green: 1.71, blue: 1.07).opacity(1)
static let infoListRowUnReadBGColor = Color(red: 2.55, green: 2.12, blue: 1.38).opacity(1)

var defaults = UserDefaults.standard

defaults.setValue(InAppDefaults.infoListRowReadBGColor, forKey: "infoListRowReadBGColor")
defaults.setValue(InAppDefaults.infoListRowUnReadBGColor, forKey: "infoListRowUnReadBGColor")

What do I need to change to get this working, read and write, using UserDefaults.standard and @AppStorage? I did try the extension methode from a posting around here, but I guess I did something very wrong, because it doesn't work with @AppStorage.

Using Xcode 13 and 14 for dev result for macOS 12 and iOS 15.

Upvotes: 4

Views: 3204

Answers (5)

TT--
TT--

Reputation: 3195

This can be achieved without importing UIKit or AppKit by:

  1. Calling resolve on a Color,
func resolve(in environment: EnvironmentValues) -> Color.Resolved

to get the RGBA components of a Color, and then

  1. Create a comma-delimited String of the components,
func commaDelimitedString(from resolvedColor: Color.Resolved) -> String {
    return resolvedColor
        .red.description + ", " + resolvedColor
        .green.description + ", " + resolvedColor
        .blue.description + ", " + resolvedColor
        .opacity.description
}
  1. Store that String with an @AppStorage variable.

To get the Color back out, I used this extension:

extension Color {

    /// Creates a `Color` from a comma-delimited String of RGBA values.
    init?(rgbaString: String) {

        let rgbaStringSplit = rgbaString.split(separator: ", ")
        guard rgbaStringSplit.count == 4 else { return nil }

        let rgbaComponents = rgbaStringSplit.map { (Double($0) ?? 0) }

        self.init(red: rgbaComponents[0], green: rgbaComponents[1], 
                  blue: rgbaComponents[2],
                  opacity: rgbaComponents[3])
    }
}

Upvotes: 0

K.pen
K.pen

Reputation: 179

Use hex strings for simplicity when saving colors with @AppStorage

extension Color {
    func toData() -> Data? {
        try? NSKeyedArchiver.archivedData(withRootObject: UIColor(self), requiringSecureCoding: false)
    }
    
    static func fromData(_ data: Data) -> Color? {
        if let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor {
            return Color(uiColor)
        }
        return nil
    }
}

Usage

struct ContentView: View {
    @State private var themeColor: Color = .white

    var body: some View {
        VStack {
            Text("Hello, World!")
                .padding()
                .background(themeColor)

            Button("Change Color") {
                // Save color
                if let colorData = Color.red.toData() {
                    UserDefaults.standard.set(colorData, forKey: "themeColor")
                }
                themeColor = .red
            }
            .onAppear {
                // Retrieve color
                if let colorData = UserDefaults.standard.data(forKey: "themeColor"),
                   let savedColor = Color.fromData(colorData) {
                    themeColor = savedColor
                }
            }
        }
    }
}

Upvotes: 1

EJZ
EJZ

Reputation: 1256

You can't by default store Color() in UserDefaults, but you can use @AppStorage and NSKeyedArchiver to achieve this result. The full example and documentation are provided in this article.

Create an extension:

import SwiftUI
import UIKit
    
    extension Color: RawRepresentable {
    public init?(rawValue: String) {
        guard let data = Data(base64Encoded: rawValue) else {
            self = .black
            return
        }

        do {
            if let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) {
                self = Color(color)
            } else {
                self = .black
            }
        } 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 ""
        }
    }
}

And use it as such:

@AppStorage("colorkey") var storedColor: Color = .black
    
    var body: some View {
        
        VStack {
            ColorPicker("Persisted Color Picker", selection: $storedColor, supportsOpacity: true)
        }
}

Upvotes: 3

iPadawan
iPadawan

Reputation: 1110

The answer that EJZ gives has put me on the right track. I've tried other methods as well, but the EJZ method I was able to use for both iOS and OSX with a little tweaking. Not wanting to edit his answer to keep that clear, I copied his part and my tweak into this answer.

import Foundation
import SwiftUI
#if os(iOS)
    import UIKit
#elseif os(OSX)
    import AppKit
#endif

Here's the file I tweaked with the OS distinction

extension Color: RawRepresentable {

    public init?(rawValue: String) {
        guard let data = Data(base64Encoded: rawValue) else {
            self = .gray
            return
        }
        do{
#if os(iOS)
            let color = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor ?? .gray
#elseif os(OSX)
            let color = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSColor ?? .gray
#endif
            self = Color(color)
        }catch{
            self = .gray
        }
    }

    public var rawValue: String {
        do{
#if os(iOS)
            let data = try NSKeyedArchiver.archivedData(withRootObject: UIColor(self), requiringSecureCoding: false) as Data
#elseif os(OSX)
            let data = try NSKeyedArchiver.archivedData(withRootObject: NSColor(self), requiringSecureCoding: false) as Data
#endif

            return data.base64EncodedString()
        }catch{
            return ""
        }
    }
}

Both works well with the ( using the code of EJZ ) the @AppStorage SwiftUI views and both systems.

@AppStorage("key") var storedColor: Color = .gray

HOWEVER: why is the size of the saved rawdata so big?

Upvotes: 2

Blacksmith
Blacksmith

Reputation: 227

you can try converting color into data and store the data instead.

here's a uikit version extending UIColor you can use it for SwiftUI's Color too

import UIKit

extension UIColor {
    class func color(data: Data) -> UIColor {
        try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! UIColor
    }

    func encode() -> Data {
        try! NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
    }
}

you can persist the color using the encode function and once you retrieve the data, you can pass it on the class func to get the color

Upvotes: 4

Related Questions