Swift Swiftui - Saving Color to UserDefaults and use it from @AppStorage

Viewed 140

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 the userDefaults and read/write by UserDefaults.standard methodes 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.default and @AppStore? I did try the extension methode from a posting around here, but I guess I do something very wrong, because it doesn't work with @AppStorage.

Using XCode 13 and 14 for dev result for MacOS 12 and iOS 15.

3 Answers

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

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 is provided from this article.

Create an extension:

import Foundation
import SwiftUI
import UIKit

extension Color: RawRepresentable {

    public init?(rawValue: String) {
        
        guard let data = Data(base64Encoded: rawValue) else{
            self = .black
            return
        }
        
        do{
            let color = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor ?? .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 ""
            
        }
        
    }

}

And use it as such:

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

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.

I hope this helps others too. Thank you EJZ and others as well as the people for reading this too.

===

To split the os's I use an import distinction in the top/Import area of the file of the scene area part.

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?

Related