Reputation: 1110
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
Reputation: 3195
This can be achieved without importing UIKit or AppKit by:
resolve
on a Color
,func resolve(in environment: EnvironmentValues) -> Color.Resolved
to get the RGBA components of a Color
, and then
func commaDelimitedString(from resolvedColor: Color.Resolved) -> String {
return resolvedColor
.red.description + ", " + resolvedColor
.green.description + ", " + resolvedColor
.blue.description + ", " + resolvedColor
.opacity.description
}
@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
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
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
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
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