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

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 {}

Related

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

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()
}

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

SwiftUI Combine Data Flow

Been away from the swift-ing for a good 3 years now.
Getting back into it now and trying to learn Combine and SwiftUI.
Making a test Workout app.
Add an exercise, record reps and weights for 3 sets.
Save data.
I'm having issues moving some data around from views to data store.
I think I'm confusing all the different property wrappers.
Summary at the bottom after code.
App:
#main
struct TestApp: App {
#StateObject private var store = ExerciseStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}
}
Views:
struct ContentView: View {
#EnvironmentObject var store: ExerciseStore
var body: some View {
List {
ForEach($store.completedExercises) { $exercise in
ExerciseView(exercise: $exercise)
}
}
}
}
struct ExerciseView: View {
#Binding var exercise: CompletedExercise
var body: some View {
VStack {
Text(exercise.exercise.name)
SetView(set: $exercise.sets[0])
SetView(set: $exercise.sets[1])
SetView(set: $exercise.sets[2])
}
}
}
struct SetView: View {
#Binding var set: ExerciseSet
var body: some View {
HStack {
TextField(
"Reps",
value: $set.reps,
formatter: NumberFormatter()
)
TextField(
"Weight",
value: $set.weight,
formatter: NumberFormatter()
)
}
}
}
Store:
class ExerciseStore: ObservableObject {
#Published var completedExercises: [CompletedExercise] = [CompletedExercise(Exercise())]
init() {
if let data = UserDefaults.standard.data(forKey: "CompletedExercise") {
if let decoded = try? JSONDecoder().decode([CompletedExercise].self, from: data) {
completedExercises = decoded
return
}
}
}
func save() {
if let encoded = try? JSONEncoder().encode(completedExercises) {
UserDefaults.standard.set(encoded, forKey: "CompletedExercise")
}
}
}
Models:
class CompletedExercise: Codable, Identifiable, ObservableObject {
var id = UUID().uuidString
var exercise: Exercise
#Published var sets = [
ExerciseSet(),
ExerciseSet(),
ExerciseSet()
]
init(exercise: Exercise) {
self.exercise = exercise
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
exercise = try container.decode(Exercise.self, forKey: .exercise)
sets = try container.decode([ExerciseSet].self, forKey: .sets)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(exercise, forKey: .exercise)
try container.encode(sets, forKey: .sets)
}
}
private enum CodingKeys: CodingKey {
case id, exercise, sets
}
struct Exercise: Codable, Identifiable {
var id = -1
var name = "Bench Press"
}
class ExerciseSet: Codable, ObservableObject {
#Published var reps: Int?
#Published var weight: Int?
init() {}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
reps = try container.decodeIfPresent(Int.self, forKey: .reps)
weight = try container.decodeIfPresent(Int.self, forKey: .weight)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(reps, forKey: .reps)
try container.encode(weight, forKey: .weight)
}
}
private enum CodingKeys: CodingKey {
case reps, weight
}
Thats more or less the current code.
I've added a bunch of print statements in the save function in ExerciseStore to see what gets saved.
No matter what I've tried, I can't get the reps/weight via the SetView text fields to persist through the ExerciseStore and get saved.
I've played around with #Binding and such as well but can't get it working.
What am I missing/messing up with the new SwiftUI data flows.
You're code is great, but the values in your ExerciseSet & CompletedExercise aren't marked with #Published.
In order to access the ObservableObject capabilities you need to publish those values enabling your Views to listen, and your class to bind to the changes made.
Also substitute ObservedObject with StateObject, same case for EnvironmentObject.
There are a couple of things I note about your code. First you declare the store as an #EnvironmentObject but you don't show the code where you set up the EnvironmentKey and EnvironmentValues to use it. It may be that you chose to leave that code out, but if you don't have it, it will be worth looking at. See the docs for EnvironmentKey, I think it explains both. https://developer.apple.com/documentation/swiftui/environmentkey.
#EnvironmentObject just declares that you want to take a property from the environment and observe it. You have to put the property in the environment (usually from a top-level view) using the environmentObject View modifier (https://developer.apple.com/documentation/swiftui/image/environmentobject(_:)/). You don't show that code either. You may have put it on your app (or whichever view instantiates ContentView).
Secondly you have #ObservableObjects but no #Published properties on those objects. #Published is how Combine knows which properties you want notifications about.
Thirdly you use #ObservedObject in a lot of your views. #ObservedObject declares that "someone else is going to own an Observable object, and they are going to give it to this view at runtime and this view is going to watch it". At some point, however, someone needs to own the object. It will be the "source of truth" that gets shared using #ObservedObject. The view that wants to own the object should declare that ownership using #StateObject. It can then pass the observed object to children and the children will use #ObservedObject to watch the object owned by the parent. If you have something that's not a observable object you want a view to own, you declare ownership using the #State modifier. Then if you want to share that state to children and let them modify it, you will use a Binding.
It's best to just have one class which is the store and make all the other model types as structs. That way things will update properly. You can flatten the model by using the IDs to cross reference, e.g.
class ExerciseStore: ObservableObject {
#Published var exercises = [Excercise]
#Published var sets
#Published var completedIDs
You can see an example of this in the Fruta sample project where there is a favourite smoothies array that is IDs and not duplicates of the smoothie structs.

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.

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)