Skip to content

Instantly share code, notes, and snippets.

@ConfusedVorlon
Created January 24, 2020 11:20
Show Gist options
  • Save ConfusedVorlon/276bd7ac6c41a99ea0514a34ee9afc3d to your computer and use it in GitHub Desktop.
Save ConfusedVorlon/276bd7ac6c41a99ea0514a34ee9afc3d to your computer and use it in GitHub Desktop.
Add Codable support to @published
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 {
public func encode(to encoder: Encoder) throws {
let mirror = Mirror(reflecting: self)
if let valueChild = mirror.children.first(where: { $0.label == "value"
}) {
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)")
}
}
else {
assertionFailure("Mirror Mirror on the wall - why no value y'all : \(self)")
}
}
}
@denizaydemir
Copy link

In Swift 5.3, self cannot be reached. valueChild becomes nil. Do you have an idea about why this happens?

@just-doit
Copy link

just-doit commented Mar 27, 2021

Just in case this is still relevant, I stumbled upon this variant, which works with the latest swift version. This is not from me, but from here: https://stackoverflow.com/questions/57444059/how-to-conform-an-observableobject-to-the-codable-protocols

extension Published: Decodable where Value: Decodable
{
    public init(from decoder: Decoder) throws
    {
        self.init(initialValue: try .init(from: decoder))
    }
}


extension Published: Encodable where Value: Encodable
{
    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 { throw EncodingError.invalidValue(self, codingPath: encoder.codingPath) }
        
        try value.encode(to: encoder)
    }
}


extension EncodingError
{
    /// `invalidValue` without having to pass a `Context` as an argument.
    static func invalidValue(_ value: Any, codingPath: [CodingKey], debugDescription: String = .init()) -> Self
    {
        .invalidValue(value, .init(codingPath: codingPath, debugDescription: debugDescription) )
    }
}

@StevenSorial
Copy link

StevenSorial commented Mar 14, 2022

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

@ConfusedVorlon
Copy link
Author

That's brilliantly simple - thank you.

I do find the language unsatisfactory in this behaviour.
It seems wrong that accessing an identical variable by putting it inside another class should change its type for the code-synthesizer...

@kudit
Copy link

kudit commented Sep 17, 2022

@Stevenmagdy , you are amazing! Your code worked perfectly! (The existing code kept crashing). Thank you!

@jolonf
Copy link

jolonf commented Apr 9, 2023

I tried @StevenSorial 's solution, however it doesn't work if the type of a @Published variable is any protocol.

e.g.:

class Parent {
  @Published var child: any Child
}

protocol Child: Codable { }

class CustomChild: Child {
  var value: String
}

The error is:

Cannot automatically synthesize 'Decodable' because 'Published <any Child>' does not conform to 'Decodable'

Do you think this is possible?

The motivation for using a protocol instead of a superclass is because using a superclass requires all of the subclasses to implement the encoders and decoders.

@jolonf
Copy link

jolonf commented Apr 9, 2023

An approach that is working instead of protocols is using enums:

class Parent {
  @Published var child: Child
}

enum Child: Codable { 
  case custom(_ child: CustomChild)
}

class CustomChild: Codable {
  var value: String
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment