Reputation: 623
In SwiftUI beta 5, Apple introduced the @Published annotation. This annotation is currently blocking this class from conforming to the Codable protocols.
How can I conform to these protocols so I can encode and decode this class to JSON? You can ignore the image property for now.
class Meal: ObservableObject, Identifiable, Codable {
enum CodingKeys: String, CodingKey {
case id
case name
case ingredients
case numberOfPeople
}
var id = Globals.generateRandomId()
@Published var name: String = "" { didSet { isInputValid() } }
@Published var image = Image("addImage")
@Published var ingredients: [Ingredient] = [] { didSet { isInputValid() } }
@Published var numberOfPeople: Int = 2
@Published var validInput = false
func isInputValid() {
if name != "" && ingredients.count > 0 {
validInput = true
}
}
}
Upvotes: 17
Views: 5438
Reputation:
As for Decodable
, we're all answering with the same thing here. Initialize the Published
with a decoded Value
.
extension Published: Decodable where Value: Decodable {
public init(from decoder: Decoder) throws {
self.init(initialValue: try .init(from: decoder))
}
}
On to Encodable
…
Unlike your average property wrapper, Published
does not employ wrappedValue
. Instead, accessing a Published
value triggers a static subscript, which allows it to call objectWillChange
on the ObservableObject
when set.
Behind the scenes, your Meal.validInput
, for example, relies on this code:
Published[
_enclosingInstance: self,
wrapped: \.validInput,
storage: \._validInput
]
_enclosingInstance
is necessary for publishing changes, when set
, but all it does for get
is specify how to access the Published
, using this:
_enclosingInstance[keyPath: storageKeyPath]
wrapped
is useless for Published
.
You always need to supply the subscript with a class instance, but this "_enclosingInstance
" does not need to be an ObservableObject
.
As such, you can store the Published
via another object, and encode its stored value like this:
public extension Published {
/// The stored value of a `Published`.
/// - Note: Only useful when not having access to the enclosing class instance.
var value: Value { Storage(self).value }
private final class Storage {
init(_ published: Published) {
self.published = published
}
var value: Value {
Published[
_enclosingInstance: self,
wrapped: \.never,
storage: \.published
]
}
/// Will never be called, but is necessary to provide a `KeyPath<Value>` for the static subscript.
private var never: Value {
get { fatalError() }
set { fatalError() }
}
/// "`var`" only because the static subscript requires a `WritableKeyPath`.
/// It will never be mutated.
private var published: Published<Value>
}
}
extension Published: Encodable where Value: Encodable {
public func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
Alternatively, you could use this for the entirety of the body of Storage
. It's just not as clear about documenting how it works.
@Published private(set) var value: Value
init(_ published: Published) {
_value = published
}
Storage
will not keep a reference to the ObservableObject
, so it's only suitable for capturing values—which is all Encodable
needs. Why Apple has not provided us with a built-in solution after all this time, I have no idea.
Upvotes: 7
Reputation: 151
A more efficient variant without Mirror
Published+Value.swift
private class PublishedWrapper<T> {
@Published private(set) var value: T
init(_ value: Published<T>) {
_value = value
}
}
extension Published {
var unofficialValue: Value {
PublishedWrapper(self).value
}
}
Published+Codable.swift
extension Published: Decodable where Value: Decodable {
public init(from decoder: Decoder) throws {
self.init(wrappedValue: try .init(from: decoder))
}
}
extension Published: Encodable where Value: Encodable {
public func encode(to encoder: Encoder) throws {
try unofficialValue.encode(to: encoder)
}
}
Upvotes: 7
Reputation: 10466
After much hacking around, I managed to add Codable directly to @Published
Note I had to update this for iOS14. This illustrates the danger of digging around in undocumented types...
Just add the code below in a file and your @Published variables will be automatically Codable (provided they are based on a Codable type)
more info here https://blog.hobbyistsoftware.com/2020/01/adding-codeable-to-published/
code here:
import Foundation
import SwiftUI
extension Published:Decodable where Value:Decodable {
public init(from decoder: Decoder) throws {
let decoded = try Value(from:decoder)
self = Published(initialValue:decoded)
}
}
extension Published:Encodable where Value:Decodable {
private var valueChild:Any? {
let mirror = Mirror(reflecting: self)
if let valueChild = mirror.descendant("value") {
return valueChild
}
//iOS 14 does things differently...
if let valueChild = mirror.descendant("storage","value") {
return valueChild
}
//iOS 14 does this too...
if let valueChild = mirror.descendant("storage","publisher","subject","currentValue") {
return valueChild
}
return nil
}
public func encode(to encoder: Encoder) throws {
guard let valueChild = valueChild else {
fatalError("Mirror Mirror on the wall - why no value y'all : \(self)")
}
if let value = valueChild.value as? Encodable {
do {
try value.encode(to: encoder)
return
} catch let error {
assertionFailure("Failed encoding: \(self) - \(error)")
}
}
else {
assertionFailure("Decodable Value not decodable. Odd \(self)")
}
}
}
Upvotes: 11
Reputation: 40639
Add the init()
and encode()
methods to your class:
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
ingredients = try values.decode([Ingredient].self, forKey: .ingredients)
numberOfPeople = try values.decode(Int.self, forKey: .numberOfPeople)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(ingredients, forKey: .ingredients)
try container.encode(numberOfPeople, forKey: .numberOfPeople)
}
Upvotes: 15