Technique for adding Codable to Published failing in iOS14

I found a blog post that makes it easy-ish to let Published and Codable co-exist. The following code has been working fine for about a year, but I just discovered it no longer works in iOS14. Before I go down a rabbit-hole of exploration, I thought I'd see if anyone off the top of their head knows what might have changed and/or what is or always was suspect about this code:

extension Published : Codable where Value : Codable {
    public init(from decoder: Decoder) throws {
        self = Published(initialValue: try Value(from: decoder))
    }
    
    public func encode(to encoder: Encoder) throws {
        let mirror = Mirror(reflecting: self)

        guard let valueChild = mirror.children.first(where: { $0.label == "value" }),
            let value = valueChild.value as? Encodable else {
                fatalError("unable to extract value from Published<\(type(of: Value.self))>")
        }
        try value.encode(to: encoder)
    }
}

So to be clear this code executes correctly in iOS13 but not iOS14.

Your code assumes that Published has a member named value that's exposed in its mirror, but Published could have renamed the value property, or removed it altogether and used some other mechanism, or started conforming to CustomReflectable. What I'm wondering is why you aren't just using wrappedValue (EDIT: directly, not through the mirror), which you know exists because it's a property wrapper…but even given that, it's not clear that this code is a good idea. See Why you can't make someone else's class Decodable: a long-winded explanation of 'required' initializers. (Sorry for the lack of formatting; that explanation predates our use of Discourse!)

wrappedValue? did you mean projectedValue? I just found someone else who had the same difficulty. here is their solution:

public func encode(to encoder: Encoder) throws {
    var copy = self
    _ = copy.projectedValue.sink(receiveValue: { (val) in
        do {
            try val.encode(to: encoder)
        } catch {
            fatalError("unable to extract value from Published<\(type(of: Value.self))>: \error)")
        }
    })
}

I am somewhat uncertain how to immediately pull the value i I want out of self in a better way. tell me if there is a way to do it that is better than the above. (I will read the article you posted shortly.)

I think the short synposis is: I've got value types which I've made Codable. I've got things I want to deal with as Published. I don't feel like I should have to write a lot of boiler plate code every time this arises. So if you've got a better suggestion how to automatically get the conformance I want, I would be thrilled to have a better cleaner safer way to do this.

Ugh, I forgot Published makes plain wrappedValue unavailable. Accessing the current value through the publisher does seem like the best you can do.

The summary of the article is "what if someone else makes Published conform to Codable, but differently", but unfortunately the conclusion is that the only 100% safe way to use Codable autosynthesis with a type you don't own is to wrap it in your own type, which in this case would mean not using Published.

Perhaps the owner of Published should add conformance to Codable rather than making everyone else fight to implement essentially the same solution everywhere?

Lots of users of Publisher (at least one, me) don't care about conforming to Codeable, in general. Why should they be bothered having to deal with conforming to Codeable when it's not necessary? And adding "do-nothing" default implementations, in my view, just causes more problems than it solves if you don't care about Codeable conformance, or if you do, having to figure out the plethora of chains of conformance, inheritance, what is being called when, etc..

But that’s not what I said. Published is different than Publisher. I know what I’m suggesting won’t happen because nobody will agree that the specific methods proposed are adequate for all cases (e.g. what do we do upon failure, I said fatalError, you say no, let’s do XXX instead...) but adding the conformance I posted doesn’t burden users of Publisher itself, right? It just makes Published, the wrapper, conform to Codable.

I am not seeing how this would spill over to burden users of Publisher. (Though other than error handling I’m not sure there’s much room for arguing about what making conformance to Codable needs to do, implementation wise. Happy to be told what I’m. Issuing here.). Again, not saying this is practical at all, just missing why it would cause a burden.

It doesn't seem appropriate for Published to be Codable in the first place, as that's not the intention of the wrapper. What are you doing where you need this? Are you making observable model objects Codable? As a workaround, implementing Codable yourself on those classes will work.

It does seem like property wrappers need a way to passthrough protocol conformances for situations like this.

class FencingPool: ObservableObject, Codable {
    @Published var bouts: [PoolBout]
    @Published var scores: [BoutScore]
    ...
}

I wish to use the class FencingPool in my app, and since it is made up of several datatypes each of which is Codable (i.e. PoolBout and BoutScore are both Codable) I want FencingPool to autosynthesize Codable so that I can easily save/restore it from state (e.g. UserDefaults) when my app starts.

By making Published Codable if T is Codable, I gain the ability to state save FencingPool without lifting a finger. That is why I want Published to conform in this manner (when T does).

1 Like
Terms of Service

Privacy Policy

Cookie Policy