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")
Related
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 this extension
extension Color {
static func hexColour(hexValue:UInt32)->Color
{
let red = Double((hexValue & 0xFF0000) >> 16) / 255.0
let green = Double((hexValue & 0xFF00) >> 8) / 255.0
let blue = Double(hexValue & 0xFF) / 255.0
return Color(red:red, green:green, blue:blue)
}
}
and the usage:
#State var cstmBackground = Color.hexColour(hexValue: 0xFF0000)
I'm storing color as string(hexValue) in Firestore. This is what I use to get the color from Firestore:
struct Spty: Identifiable{
var id: String = UUID().uuidString
var spty: String
var color: String
}
class SptyViewModel: NSObject, ObservableObject{
#Published var specialities = [Spty]()
func fetchData(){
let db = Firestore.firestore()
db.collection("specialities").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {return }
self.specialities = documents.map { (queryDocumentSnapshot) -> Spty in
let data = queryDocumentSnapshot.data()
let spty = data["spty"] as? String ?? ""
let color = data["color"] as? String ?? ""
return Spty(spty: spty, color: color)
}
}
}
}
To display different colors according to the hex value stored in Firestore, I need to connect both color in SptyViewModel and (hexValue: xxxx), but didn't manage to. How to do it?
EDIT:
What appears on debug:
2020-12-17 15:28:02.590860-0300 MedApp[4917:50349] 6.34.0 -
[Firebase/Analytics][I-ACS023007] Analytics v.60900000 started
2020-12-17 15:28:02.592600-0300 MedApp[4917:50349] 6.34.0 -
[Firebase/Analytics][I-ACS023008] To enable debug logging set the
following application argument: -FIRAnalyticsDebugEnabled (see
goo.gl/RfcP7r) info successful info successful info successful
2020-12-17 15:28:05.439911-0300 MedApp[4917:50347] 6.34.0 -
[Firebase/Analytics][I-ACS800023] No pending snapshot to activate. SDK
name: app_measurement 2020-12-17 15:28:05.459475-0300
MedApp[4917:50344] 6.34.0 - [Firebase/Analytics][I-ACS023012]
Analytics collection enabled 2020-12-17 15:28:05.459898-0300
MedApp[4917:50344] 6.34.0 - [Firebase/Analytics][I-ACS023220]
Analytics screen reporting is enabled. Call +[FIRAnalytics
logEventWithName:FIREventScreenView parameters:] to log a screen view
event. To disable automatic screen reporting, set the flag
FirebaseAutomaticScreenReportingEnabled to NO (boolean) in the
Info.plist field error field error field error field error
All fields are strings
extension Color {
init?(rgb: [String: Int]) {
if let r = rgb["r"],
let g = rgb["g"],
let b = rgb["b"] {
let red = Double(r)/255
let green = Double(g)/255
let blue = Double(b)/255
self.init(red: red, green: green, blue: blue)
} else {
return nil
}
}
}
struct Spty: Identifiable {
let id = UUID().uuidString
let spty: String
let color: Color?
init(spty: String, rgbMap: [String: Int]) {
self.spty = spty
self.color = Color(rgb: rgbMap)
}
}
class SptyViewModel: NSObject, ObservableObject {
#Published var specialities = [Spty]()
func fetchData(){
Firestore.firestore().collection("specialities").addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
return
}
for doc in snapshot.documents {
if let spty = doc.get("spty") as? String,
let color = doc.get("color") as? [String: Int] {
let s = Spty(spty: spty, rgbMap: color)
self.specialities.append(s)
} else {
print("field error")
}
}
}
}
}
After reading the answers given and after some researches and tests, I finally got it.
First, I stored, in Firestore, fields "r", "g" and "b", each one as number and with max value 255
Then, I added the following code in my existent func fetchData():
struct Spty: Identifiable{
var id: String
var spty: String
var r: NSNumber
var g: NSNumber
var b: NSNumber
}
class SptyViewModel: NSObject, ObservableObject{
#Published var specialities = [Spty]()
#Published var search = ""
func fetchData(){
let db = Firestore.firestore()
db.collection("specialities").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot else {return }
self.specialities = documents.documents.compactMap { (doc) -> Spty? in
let id = doc.documentID
let spty = doc.get("spty") as! String
let r = doc.get("r") as! NSNumber
let g = doc.get("g") as! NSNumber
let b = doc.get("b") as! NSNumber
return Spty(id: id, spty: spty, r: r , g: g , b: b )
}
}
}
}
Then, inside the ForEach, I used:
.background(Color(UIColor(red: CGFloat(truncating: spty.r) / 255.0, green: CGFloat(truncating: spty.g) / 255.0, blue: CGFloat(truncating: spty.b) / 255.0, alpha: 1.0)))
There was no need to have an extension Color
I have the following struct:
struct Note: Identifiable, Codable {
enum CodingKeys: CodingKey {
case title, message, background
}
let id = UUID()
let title: String
let message: String
let background: NoteBackground
func copy(with zone: NSZone? = nil) -> Any {
let copy = Note(title: title, message: message, background: background)
return copy
}
}
What I would like to be able to do is to be able to copy and modify an object, with an id other than the copied object.
let note = Note(
title: "Title",
message: "Message",
background: .url("https://a.wattpad.com/useravatar/climaxmite.256.718018.jpg")
)
let note2 = note
.copy()//clone
.edit(background: .gradient([
Color(red: 0, green: 0.95, blue: 1),
Color(red: 0.56, green: 0.18, blue: 0.89)
])) as! Note
How can I do any advice?
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.
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.