How to make `ObservableObject` with `#Published` properties `Codable`? - swift

My codable observable Thing compiles:
class Thing: Codable, ObservableObject {
var feature: String
}
Wrapping feature in #Published though doesn’t:
class Thing: Codable, ObservableObject {
#Published var feature: String
}
🛑 Class 'Thing' has no initializers
🛑 Type 'Thing' does not conform to protocol 'Decodable'
🛑 Type 'Thing' does not conform to protocol 'Encodable'
Apparently Codable conformance can’t be synthesized anymore because #Published doesn’t know how to encode/decode its wrappedValue (because it doesn’t conform to Codable even if its wrapped value does)?
Okay, so I’ll let it know how to do it!
extension Published: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue) // 🛑 'wrappedValue' is unavailable: #Published is only available on properties of classes
}
public init(from decoder: Decoder) throws {
var container = try decoder.singleValueContainer()
wrappedValue = try container.decode(Value.self) // 🛑 'wrappedValue' is unavailable: #Published is only available on properties of classes
}
}
So sorrow, much misery, many sadness! 😭
How can I easily add back Codable synthesis (or similar) without defining encode(to:) and init(from:)?

I don't know why you would want to do it, I think you should code the struct, not the published class. Something like this:
struct CodableStruct: Codable {
var feature1: String = ""
var feature2: Int = 0
}
class Thing: ObservableObject {
#Published var features: CodableStruct = .init()
}

Related

Observable Object breaks Codable protocol compliance in observing struct despite conforming to Codable (SwiftUI)

I am confronted with a bit of a conundrum. I have an Observable Object MatrixStatus that is codable:
class MatrixStatus: ObservableObject, Codable{
#Published var recalcTriggerd = false
init(){}
//Codable protocol
enum CodingKeys: CodingKey{
case recalcTriggered
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(recalcTriggerd, forKey: .recalcTriggered)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
recalcTriggerd = try container.decode(Bool.self, forKey: .recalcTriggered)
}
}
It is set up as an environment object by a View of a document. The document observes it and remains codable. However, if I observe it in a different struct, MatrixData, that is a property of the document, the MatrixData struct stops conforming to Codable
struct MatrixData: Codable, Equatable{
#EnvironmentObject var calcStatus: MatrixStatus //triggers 'does not conform to codable'
...
Any suggestions how to solve, other than implementing the encode etc functions in MatrixData?
Thanks,
Lars

using DidSet or WillSet on decodable object

I currently have a Codable object which I am initializing using:
let decoder = JSONDecoder()
let objTest: TestOBJClass = try! decoder.decode(TestOBJClass.self, from: data)
TestOBJClass:
public class TestOBJClass: Codable {
var name: String
var lastname: String?
var age: Int? {
DidSet {
//Do Something
}
}
}
The object gets initialized with no issues. However, the DidSet function on the age variable is not getting called. Are we able to use these functions when working with Decodable or Encodable objects? Thank you for your help!
As #Larme pointed out in comments, property observers like didSet aren't called in initializers, including the one for Codable conformance. If you want that behavior, refactor the code from your didSet into a separate named function, and then at the end of your initializer call that function.
struct MyStruct: Codable
{
...
var isEnabled: Bool {
didSet { didSetIsEnabled() }
}
func didSetIsEnabled() {
// Whatever you used to do in `didSet`
}
init(from decoder: Decoder) throws
{
defer { didSetIsEnabled() }
...
}
}
Of course, that does mean you'll need to explicitly implement Codable conformance rather than relying on the compiler to synthesize it.

Codable conformance at #Published array of a codable-conformed Struct

I am tinkering around with URLSession and was essentially building a super simple app to load a JSON file into ContentView sort of like a friends list from Facebook and wanted some clarity on not any errors I'm having, but rather, the internal workings of Swift's Codable protocol. Here are some code and explanations:
struct User: Identifiable, Codable {
struct Friend : Identifiable, Codable {
var name : String
var id : String
}
var id : String
var isActive: Bool
var name : String
var age: Int
var company : String
var address : String
var about : String
var registered : String
var friends : [Friend]
var checkIsActive: String {
return self.isActive ? "🟢" :"🔴"
}
}
So to summarize above, I have a User struct which contains a bunch of properties that conform to Codable.
class UsersArrayClass: ObservableObject {
#Published var userArray = [User]()
}
However, I have another class, UsersArrayClass which creates a #Published var userArray of User struct objects. This class conforms to the #ObservableObject protocol, but of course, when I try to make it conform to Codable, it does not like that likely because of the #Published property wrapper being applied on the array itself... This is what is essentially confusing me though if the User struct has Codable conformance, why doesn't userArray which contains User objects automatically conform to Codable as well?
I was thinking that maybe loading all of this up into a Core Data model would solve my problems, but I still can't move on unless I understand what I'm missing out on here, so thanks in advance for any input.
It is hacky, but we can via extension add Codable conformance to Published despite lacking access to Published internals.
extension Published: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
guard
let storageValue =
Mirror(reflecting: self).descendant("storage")
.map(Mirror.init)?.children.first?.value,
let value =
storageValue as? Value
??
(storageValue as? Publisher).map(Mirror.init)?
.descendant("subject", "currentValue")
as? Value
else { fatalError("Failed to encode") }
try value.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
self.init(initialValue: try .init(from: decoder))
}
}
Quick check:
class User: ObservableObject, Codable {
#Published var name = "Paul"
}
struct ContentView: View {
#ObservedObject var user = User()
var body: some View {
let data = try? JSONEncoder().encode(user)
let dataFromStr = """
{
"name": "Smith"
}
"""
.data(using: .utf8)
let decoded = try! JSONDecoder().decode(User.self, from: dataFromStr!)
return
VStack{
Text(verbatim: String(data: data!, encoding: .utf8) ?? "encoding failed")
Text(decoded.name)
}
}
}
/*
Cannot automatically synthesize 'Encodable' because 'Published<[User]>'
does not conform to 'Encodable' #Published var userArray = [User]()
*/
// Published declaration
#propertyWrapper struct Published<Value> { ... }
Published don't conform to Codable or any common protocol in Foundation currently
Trying to make Published conform to Codeable resulting error below:
/*
Implementation of 'Decodable' cannot be
automatically synthesized in an extension in a different file to the type
*/
extension Published: Codable where Value: Codable {}

Codable variable in Codable Class

Given this class:
class MyClass: Codable {
var variable : Codable? = nil
}
I get error:
Type 'MyClass' does not conform to protocol 'Decodable'
Type 'MyClass' does not conform to protocol 'Encodable'
How can I have a generic variable that conforms to Codable as a property in a Codable class?
May be like this if i got your question correctly
class MyClass<T: Codable>: Codable {
var variable : T? = nil
}
If you try to implement your decoder you will have something like:
class MyClass: Codable {
var variable: Codable?
enum MyClassKeys: String, CodingKey {
case variable = "variable"
}
required init(from decoder: Decoder) {
let container = try! decoder.container(keyedBy: MyClassKeys.self)
variable = try! container.decode(XXXX, forKey: .variable)
}
}
But instead of XXXX, you should put a class type (for example String.self). What can you put here? Its not possible for the compiler to guess what you want. Instead of Codable type maybe you can put a class which conform to Codable protocol. So you can have something like this:
class MyClass: Codable {
var variable: MyClass?
enum MyClassKeys: String, CodingKey {
case variable = "variable"
}
required init(from decoder: Decoder) {
let container = try! decoder.container(keyedBy: MyClassKeys.self)
variable = try! container.decode(MyClass.self, forKey: .variable)
}
}
You can't.
But as a workaround, you can create an enum conforming to Codable.
enum SubType: Codable {
case foo
case bar
}

A codable structure contains a protocol property

I have a protocol which is inherited from codable
protocol OrderItem:Codable {
var amount:Int{get set}
var isPaid:Bool{get set}
}
And a struct conform this protocol
struct ProductItem:OrderItem {
var amount = 0
var isPaid = false
var price = 0.0
}
However when I put this structure into a codable structure, I got errors
struct Order:Codable {
var id:String
var sn:String = ""
var items:[OrderItem] = []
var createdAt:Int64 = 0
var updatedAt:Int64 = 0
}
The errors are
Type 'Order' does not conform to protocol 'Encodable'
Type 'Order' does not conform to protocol 'Decodable'
But if I change items:[OrderItem] to items:[ProductItem] , everything works!
How can I fix this problem?
You cannot do that because a protocol only states what you must do. So when you conform your protocol X to Codable, it only means that any type that conforms to X must also conform to Codable but it won't provide the required implementation. You probably got confused because Codable does not require you to implement anything when all your types already are Codable. If Codable asked you to, say, implement a function called myFunction, your OrderItem would then lack the implementation of that function and the compiler would make you add it.
Here is what you can do instead:
struct Order<T: OrderItem>: Codable {
var id:String
var sn:String = ""
var items: [T] = []
var createdAt:Int64 = 0
var updatedAt:Int64 = 0
}
You now say that items is a generic type that conforms to OrderItem.
It's worth mentioning that if you have a property of an array and the type is a protocol: let arrayProtocol: [MyProtocol] and the array contains multiple types that all conform to MyProtocol, you will have to implement your own init(from decoder: Decoder) throws to get the values and func encode(to encoder: Encoder) throws to encode them.
So for example:
protocol MyProtocol {}
struct FirstType: MyProtocol {}
struct SecondType: MyProtocol {}
struct CustomObject: Codable {
let arrayProtocol: [MyProtocol]
enum CodingKeys: String, CodingKey {
case firstTypeKey
case secondTypeKey
}
}
so our decode will look like this:
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
// FirstType conforms to MyProtocol
let firstTypeArray = try values.decode([FirstType].self, forKey: .firstTypeKey)
// SecondType conforms to MyProtocol
let secondTypeArray = try values.decode([SecondType].self, forKey: .secondTypeKey)
// Our array is finally decoded
self.arrayProtocol: [MyProtocol] = firstTypeArray + secondTypeArray
}
and the same for encoded, we need to cast to the actual type before encoding:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let firstActualTypeArray = arrayProtocol.compactMap{$0 as? FirstType}
let secondActualTypeArray = arrayProtocol.compactMap{$0 as? SecondType}
try container.encode(firstActualTypeArray, forKey: .firstTypeKey)
try container.encode(secondActualTypeArray, forKey: .secondTypeKey)
}
Codable is a type alias for Encodable and Decodable.
Therefore if you're implementing it you need to implement the following two functions.
func encode(to: Encoder)
init(from: Decoder)