struct Task: Codable {
var content: String
var deadline: Date
var color: UIColor
...
}
There are warnings saying "Type 'Task' does not conform to protocol 'Decodable'" and "Type 'Task' does not conform to protocol 'Encodable'". I searched and found that this is because UIColor does not conform to Codable. But I have no idea how to fix that. So...
How to make UIColor Codable?
If you care only about the 4 color components this is a simple solution using a wrapper struct
struct Color : Codable {
var red : CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
var uiColor : UIColor {
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
init(uiColor : UIColor) {
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
}
}
In this case you have to write a custom initializer to convert the 4 color components from Color to UIColor and vice versa.
struct MyTask: Codable { // renamed as MyTask to avoid interference with Swift Concurrency
private enum CodingKeys: String, CodingKey { case content, deadline, color }
var content: String
var deadline: Date
var color : UIColor
init(content: String, deadline: Date, color : UIColor) {
self.content = content
self.deadline = deadline
self.color = color
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
content = try container.decode(String.self, forKey: .content)
deadline = try container.decode(Date.self, forKey: .deadline)
color = try container.decode(Color.self, forKey: .color).uiColor
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(content, forKey: .content)
try container.encode(deadline, forKey: .deadline)
try container.encode(Color(uiColor: color), forKey: .color)
}
}
Now you can encode and decode UIColor
let task = MyTask(content: "Foo", deadline: Date(), color: .orange)
do {
let data = try JSONEncoder().encode(task)
print(String(data: data, encoding: .utf8)!)
let newTask = try JSONDecoder().decode(MyTask.self, from: data)
print(newTask)
} catch { print(error) }
A smart alternative for Swift 5.1 and higher is a property wrapper
#propertyWrapper
struct CodableColor {
var wrappedValue: UIColor
}
extension CodableColor: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid color"
)
}
wrappedValue = color
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
try container.encode(data)
}
}
and mark the property with #CodableColor
struct MyTask: Codable {
var content: String
var deadline: Date
#CodableColor var color: UIColor
...
}
Here's a solution which I've published as a Swift Package which will work for any color in any color space (even fancy system colors like label and windowBackground!), and any other NSCoding object!
It's relatively easy to use:
import SerializationTools
let color = UIColor.label
let encodedData = try color.codable.jsonData()
// UTF-8 encoded Base64 representation of the `NSCoding` data
let decodedColor = try UIColor.CodableBridge(jsonData: encodedData).value
And remember that this even works with the fancy magical colors like .label and .systemBackground!
Of course, you can also use it like any other Swift codable, such as placing it in a struct with auto-synthesized Codable conformance or using it with JSONEncoder/JSONDecoder:
import SerializationTools
struct Foo: Codable {
let color: UIColor.CodableBridge
init(color: UIColor) {
self.color = color.codable
}
}
import SerializationTools
let fooInstance = Foo(color: .systemPurple)
let encoder = JSONEncoder()
let encodedData = try encoder.encode(fooInstance)
let decoder = JSONDecoder()
let decodedFoo = try decoder.decode(Foo.self, from: encodedData)
This will work with NSColor, too, as well as anything else that conforms to NSCoding, such as NSImage/UIImage, MKMapView, GKAchievement, and much more!
I use UIColor subclass
final class Color: UIColor, Decodable {
convenience init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let hexString = try container.decode(String.self)
self.init(hex: hexString)
}
}
Thus, there is no need for each class or structure to implement the functions of the Decodable protocol. It seems to me that this is the most convenient way, especially when there can be many color parameters in one class or structure.
You can implement Encodable in the same way if it's necessary.
I solved this issue with a custom class that allowed automatic conformance to codable. This is beneficial as it prevents writing custom conformance to codable. It also makes it easier to work with UIColor and and CGColor
class Color:Codable{
private var _green:CGFloat
private var _blue:CGFloat
private var _red:CGFloat
private var alpha:CGFloat
init(color:UIColor) {
color.getRed(&_red, green: &_green, blue: &_blue, alpha: &alpha)
}
var color:UIColor{
get{
return UIColor(red: _red, green: _green, blue: _blue, alpha: alpha)
}
set{
newValue.getRed(&_red, green:&_green, blue: &_blue, alpha:&alpha)
}
}
var cgColor:CGColor{
get{
return color.cgColor
}
set{
UIColor(cgColor: newValue).getRed(&_red, green:&_green, blue: &_blue, alpha:&alpha)
}
}
}
We can make UIColor and all of its descendants Codable.
import UIKit
extension Decodable where Self: UIColor {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let components = try container.decode([CGFloat].self)
self = Self.init(red: components[0], green: components[1], blue: components[2], alpha: components[3])
}
}
extension Encodable where Self: UIColor {
public func encode(to encoder: Encoder) throws {
var r, g, b, a: CGFloat
(r, g, b, a) = (0, 0, 0, 0)
var container = encoder.singleValueContainer()
self.getRed(&r, green: &g, blue: &b, alpha: &a)
try container.encode([r,g,b,a])
}
}
extension UIColor: Codable { }
Check it
import XCTest
class ColorDescendant: UIColor { }
let testColor = ColorDescendant.green
class CodingTextCase: XCTestCase {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
func testUIColor() throws {
let colorAsJSON = try encoder.encode(UIColor.red)
print(String(data: colorAsJSON, encoding: .utf8)!)
let uiColor = try? decoder.decode(UIColor.self, from: colorAsJSON)
XCTAssertEqual(uiColor!, UIColor.red)
}
func testUIColorDescendant() throws {
let colorAsJSON = try encoder.encode(testColor)
print(String(data: colorAsJSON, encoding: .utf8)!)
let uiColor = try? decoder.decode(ColorDescendant.self, from: colorAsJSON)
XCTAssertEqual(uiColor!, testColor)
}
}
CodingTextCase.defaultTestSuite.run()
This solution requires only 9 bytes for data storage while more generalized one will require about 500 bytes.
Related
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.
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?
How can I retrieve the color value from Firebase from String to color in swift
I am a beginner, I have managed to save the color value in Firebase
But I could not retrieve it to the application
This value is in Firebase
Color: "0.424215 0.911966 0.273487 1"
This is the code that I was able to convert the color value to string and then I set it down in Firebase
let cgColor = self.ColorIsColor!.cgColor
self.labelColor = CIColor(cgColor: cgColor).stringRepresentation
Here I want to restore color on detailLabel.textColor
class CellDetiles: UITableViewCell {
var labelColor = String()
var ColorIsColor : UIColor?
#IBOutlet weak var imageLabel: UIImageView!
#IBOutlet weak var detailLabel: UITextView!
#IBOutlet weak var qusLabel: UILabel!
var Dat : DetilesVC?
var arrQR : QRModel? {
didSet {
SetupQR()
}
}
func SetupQR () {
qusLabel.text = arrQR?.sub
detailLabel.text = arrQR?.detiles
detailLabel.textColor = ""
if let stringImage = arrQR?.ImgSub {
let image = URL(string: stringImage)
self.imageLabel.sd_setImage(with : image)
}
}
}
Please help me
What you can do is split the String by spaces and separate each component out:
let rgba = labelColor.split(separator: " ")
let r = Double(rgba[0]) ?? 0
let g = Double(rgba[1]) ?? 0
let b = Double(rgba[2]) ?? 0
let a = Double(rgba[3]) ?? 0
let finalColor = UIColor(red: CGFloat(r),
green: CGFloat(g),
blue: CGFloat(b),
alpha: CGFloat(a))
You can put this wherever you need to decode the color, but it would probably be better to put it in a UIColor initializer instead:
extension UIColor {
convenience init(string: String) {
let rgba = labelColor.split(separator: " ")
let r = Double(rgba[0]) ?? 0
let g = Double(rgba[1]) ?? 0
let b = Double(rgba[2]) ?? 0
let a = Double(rgba[3]) ?? 0
self.init(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(a))
}
}
That way you can just call UIColor(string: colorString) whenever you need to decode it.
How you load the color is determined by how you store it in the first place. NSData objects are quite convenient as they can be encoded, cast to a string object etc which is easily stored in firebase.
I would suggest a simple extension to NSColor
extension NSColor {
func saveToFirebase() -> String {
let data = try! NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
let dataAsString = data.base64EncodedString()
return dataAsString
}
func getFromFirebase(stringData: String) -> NSColor {
let colorData = Data(base64Encoded: stringData)!
let color = try! NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: colorData)
return color!
}
}
then to save a color, do this
let red = NSColor.red
let redColorDict: [String: Any] = [
"name": "red",
"color_string": red.saveToFirebase()
]
colorsRef.child("red_color").setValue(redColorDict)
and then to read back in do this to read the red color and set the background of a view to red
func readColors() {
let redRef = self.ref.child("colors").child("red_color") //self.ref points to my firebase
redRef.observeSingleEvent(of: .value, with: { snapshot in
if let dataAsString = snapshot.childSnapshot(forPath: "color_string").value as? String {
self.myView.layer?.backgroundColor = NSColor().getFromFirebase(stringData: dataAsString).cgColor
}
})
}
Note there is NO error checking in the above for brevity so be sure to handle the optionals safely.
Also note this is macOS, but it would apply to iOS with UIColor.
func getColor(_ str: String) -> UIColor? {
let colorData = str
.components(separatedBy: .whitespaces)
.compactMap { Double($0) }
.compactMap { CGFloat($0) }
guard colorData.count == 4 else {
return nil
}
return UIColor(displayP3Red: colorData[0], green: colorData[1], blue: colorData[2], alpha: colorData[3])
}
I have tried to implement the solution by Vadian here: Make UIColor Codable
But I am getting an error that I haven't been able to overcome.
Here is my implementation of the above mention solution:
struct Color : Codable {
var red : CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
var uiColor : UIColor {
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
init(uiColor : UIColor) {
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
}
}
struct Tasting: Codable {
private enum CodingKeys: String, CodingKey { case id, title, color, textColor, notes }
var id: Int
var title: String
var color : UIColor
var textColor : UIColor
var notes: String
init(id: Int, title: String, color : UIColor, textColor : UIColor, notes: String) {
self.id = id
self.title = title
self.color = color
self.textColor = textColor
self.notes = notes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
color = try container.decode(Color.self, forKey: .color).uiColor
textColor = try container.decode(Color.self, forKey: .textColor).uiColor
notes = try container.decode(String.self, forKey: .notes)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(title, forKey: .title)
try container.encode(Color(uiColor: color), forKey: .color)
try container.encode(Color(uiColor: color), forKey: .textColor)
try container.encode(notes, forKey: .notes)
}
}
And here:
//Encodes UIColor so it can be saved
let tastings = [
Tasting(id: 0, title: "(Delete this row after you add your first tasting!)", color: .green, textColor: .black, notes: "Add notes here.")
]
do {
let data = try JSONEncoder().encode(tastings)
print(String(data: data, encoding: .utf8)!)
let newTastings = try JSONDecoder().decode(Tasting.self, from: data)
print("newTastings \(newTastings)")
} catch {
print("newTastings \(error)")
}
Then saving it to UserDefaults.
//Saves new brand to device memory
let savedTastings = tastings
UserDefaults.standard.set(savedTastings, forKey: "tastings")
These are the errors I am getting:
newTastings typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
2019-07-25 11:38:05.909711-0700 WhiskyTasting[10601:3581697] [User Defaults] Attempt to set a non-property-list object (
"WhiskyTasting.Tasting(id: 0, title: \"(Delete this row after you add your first tasting!)\", color: UIExtendedSRGBColorSpace 0 1 0 1, textColor: UIExtendedGrayColorSpace 0 1, notes: \"Add notes here.\")"
) as an NSUserDefaults/CFPreferences value for key tastings
2019-07-25 11:38:05.910207-0700 WhiskyTasting[10601:3581697] ***
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object (
"WhiskyTasting.Tasting(id: 0, title: \"(Delete this row after you add your first tasting!)\", color: UIExtendedSRGBColorSpace 0 1 0 1, textColor: UIExtendedGrayColorSpace 0 1, notes: \"Add notes here.\")"
) for key tastings'
I hope y'all see a simple typo that I'm missing. Been struggling with variations of this for a few days.
The errors are not related to my solution.
The first error tells you that the object is an array. Please read your code, tastings is clearly an array.
So you have to decode an array
let newTastings = try JSONDecoder().decode([Tasting].self, from: data)
The second error tells you that in your struct is a type which is not property list compliant. This type is UIColor. You cannot save Tasting instances to UserDefaults, but you can save JSON-/ or PropertyList-encoded Tasting instances.
let data = try JSONEncoder().encode(tastings)
UserDefaults.standard.set(data, forKey: "tastings")
I wrote an extension of UIColor to change alpha directly:
public extension UIColor {
public var rgbaComponents: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var components: [CGFloat] {
let c = cgColor.components!
if c.count == 4 {
return c
}
return [c[0], c[0], c[0], c[1]]
}
let r = components[0]
let g = components[1]
let b = components[2]
let a = components[3]
return (red: r, green: g, blue: b, alpha: a)
}
public var alpha: CGFloat {
get {
return cgColor.alpha
}
set {
var rgba = rgbaComponents
self = UIColor(red: rgba.red // error here: "cannot assign to value: 'self' is immutable"
, green: rgba.green, blue: rgba.blue, alpha: newValue)
}
}
}
but there is an error:
cannot assign to value: 'self' is immutable
But extension of Date to assign to self is OK
public extension Date {
public var day: Int {
get {
return Calendar.current.component(.day, from: self)
}
set {
let allowedRange = Calendar.current.range(of: .day, in: .month, for: self)!
guard allowedRange.contains(newValue) else { return }
let currentDay = Calendar.current.component(.day, from: self)
let daysToAdd = newValue - currentDay
if let date = Calendar.current.date(byAdding: .day, value: daysToAdd, to: self) {
self = date // This is OK
}
}
}
}
Is it because of UIColor is from NSObject and Date is a Swift struct? What is the root cause?
You're correct it is because Date is a struct (value type) and UIColor is a class (reference type).
When you assign to self for a struct you are really just updating all the properties of that structure (simply put) so the actual memory location doesn't change. So you are not actually mutating the value of self itself.
However when you assign to self for a class you are creating an entire new class in memory so when you assign it to self you are trying to mutate self. Even if it was allowed how would anything holding a reference to the colour handle it as they would still be holding a reference to the original class.
Is it possible to extend NSDecimalNumber to conform Encodable & Decodable protocols?
It is not possible to extend NSDecimalNumber to conform to Encodable & Decodable protocols. Jordan Rose explains it in the following swift evolution email thread.
If you need NSDecimalValue type in your API you can build computed property around Decimal.
struct YourType: Codable {
var decimalNumber: NSDecimalNumber {
get { return NSDecimalNumber(decimal: decimalValue) }
set { decimalValue = newValue.decimalValue }
}
private var decimalValue: Decimal
}
Btw. If you are using NSNumberFormatter for parsing, beware of a known bug that causes precision loss in some cases.
let f = NumberFormatter()
f.generatesDecimalNumbers = true
f.locale = Locale(identifier: "en_US_POSIX")
let z = f.number(from: "8.3")!
// z.decimalValue._exponent is not -1
// z.decimalValue._mantissa is not (83, 0, 0, 0, 0, 0, 0, 0)
Parse strings this way instead:
NSDecimalNumber(string: "8.3", locale: Locale(identifier: "en_US_POSIX"))
In swift you should use Decimal type. This type confirms to protocols Encodable & Decodable from the box.
If you have NSDecimalNumber type in your code it's easy to cast it to Decimal
let objcDecimal = NSDecimalNumber(decimal: 10)
let swiftDecimal = (objcDecimal as Decimal)
With Swift 5.1 you can use property wrappers to avoid the boilerplate of writing a custom init(from decoder: Decoder) / encode(to encoder: Encoder).
#propertyWrapper
struct NumberString {
private let value: String
var wrappedValue: NSDecimalNumber
init(wrappedValue: NSDecimalNumber) {
self.wrappedValue = wrappedValue
value = wrappedValue.stringValue
}
}
extension NumberString: Decodable {
init(from decoder: Decoder) throws {
value = try String(from: decoder)
wrappedValue = NSDecimalNumber(string: value)
}
}
extension NumberString: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue.stringValue)
}
}
extension NumberString: Equatable {}
Usage:
struct Foo: Codable {
#NumberString var value: NSDecimalNumber
}
In my case, We are maintaining legacy code which is Objective-C and Swift
One of the modules we needed to have a property of type NSNumber (internal API reason) which is not supported by Codable
So We use Codable for almost all supported data types and NSCoding with a help of NSKeyedUnarchiver for unsupported types
I am sharing here a sample of the code, as a reference that might help
someone who has a such scenario.
class Branch: NSObject, Codable {
#objc var discountMaxLimit: NSNumber?
private enum CodingKeys: String, CodingKey {
case discountInfoKeys
}
private enum CorporateDiscountInfoKeys: String, CodingKey {
case discountMaxLimit = "discount_max_limit"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var discountInfoK = container.nestedContainer(keyedBy: discountInfoKeys.self, forKey: .discountInfoKeys)
if let value = discountMaxLimit {
let data = try NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false)
try discountInfoK.encode(data, forKey: .discountMaxLimit)
}
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let discountInfo = try container.nestedContainer(keyedBy: discountInfoKeys.self, forKey: .discountInfoKeys)
let discountMaxLimitData = try discountInfo.decode(Data.self, forKey: .discountMaxLimit)
discountMaxLimit = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(discountMaxLimitData) as? NSNumber
}
}