2

I am trying to save a configuration data structure with UserDefaults, thus the data structure needs to conform to the Codable protocol. This is my data structure:

// Data structure which saves two objects, which conform to the Connection protocol
struct Configuration {
    var from: Connection
    var to: Connection
}

protocol Connection: Codable {
    var path: String { get set }
}


// Two implementations of the Connection protocol
struct SFTPConnection: Connection, Codable {
    var path: String
    var user: String
    var sshKey: String
}

struct FTPConnection: Connection, Codable {
    var path: String
    var user: String
    var password: String
}

If I just add Codable to Configuration, it won't work. So I have to implement it myself.

extension Configuration: Codable {

    enum CodingKeys: String, CodingKey {
        case from, to
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let from = try container.decode(Connection.self, forKey: .from)
        let to = try container.decode(Connection.self, forKey: .to)

        self.from = from
        self.to = to
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(from, forKey: .from)
        try container.encode(to, forKey: .to)
    }
}

For every call on decode() or encode() I get the error Protocol type 'Connection' cannot conform to 'Decodable/Encodable' because only concrete types can conform to protocols.

I can see that it is difficult for the compiler to identify, which class should be used to decode the given object. But I figured it should be easy to encode an object, since every object of type Connection implements the encode() method.

I know, that the problem lies with the protocol and that the protocol can't be used with Decodable/Encodable. How would I change the code in decode/encode, so that I can still use the protocol with the various implementations? My guess is to somehow tell decode/encode which implementation of the protocol to use. I would appreciate any elegant solutions for this problem!

5
  • The error is right: Codable requires concrete classes conforming to Codable. There is no workaround except generics, which in practice provides a concrete type when the code is executed. Commented Dec 16, 2019 at 9:22
  • Thanks, could you explain the idea with generics in more detail? Commented Dec 16, 2019 at 9:24
  • See Jeremy's answer. That's exactly what I mean Commented Dec 16, 2019 at 9:49
  • I assume there is a typo in the question: the protocol is defined as Connection but, in the extension, it is referred to as ConnectionConfiguration Commented Dec 16, 2019 at 10:00
  • Thanks, that happens when I try to simplify my code for a question ... Commented Dec 16, 2019 at 10:05

1 Answer 1

3

It's a limitation of Swift that a protocol cannot conform to itself. Thus from and to do not conform to Codable as bizarre as that seems.

You can get around it by using generics which basically means you declare from and to as arbitrary types that conform to Codable. Here's how:

struct Configuration<F: Connection, T: Connection>: Codable {
    var from: F
    var to: T
}


let myFrom = SFTPConnection(path: "foo", user: "me", sshKey: "hgfnjsfdjs")
let myTo = FTPConnection(path: "foo", user: "me", password: "hgfnjsfdjs")
let example = Configuration(from: myFrom, to: myTo)

So F and T are types that conform to Connection. When you instantiate example in the last line, the compiler infers F is SFTPConnection and T is FTPConnection.

Once I added the generic parameters, Configuration was able to synthesise the conformance to Codable without the extension.


To answer Sh_kahn's point about having two generic parameters, I did this to allow from and to to be connections of different types. If you always want the two connections to be of the same type i.e. always two SFTPConnections or two FTPConnections you should declare the Configuration like this:

struct Configuration<C: Connection>: Codable {
    var from: C
    var to: C
}
Sign up to request clarification or add additional context in comments.

5 Comments

there is no difference between that and making them properties also you repeat the same <F: Connection, T: Connection> it could be 1
@Sh_Khan Making what properties? I used two generic parameters so that the from property could be of a different type to the to property.
Thanks, that kind of solves the question. But what would I do I wanted to load all my Configuration objects from the UserDefaults, which could all possibly have a different type F and T? I would call func loadAll<F:Connection, T: Connection>() -> [String: Configuration<F, T>]? { return UserDefaults.standard.dictionaryRepresentation() as? [String: Configuration<F, T>] } But this wouldn't work since every Configuration could have different F and T
@Codey I don't know how to solve that particular issue. By the time you get to decoding it, you're already in the object's init which means the F and T have already been chosen. This might be a case for using classes and inheritance. But I would suggest writing a new question for it.
@JeremyP I posted a new question for this problem: stackoverflow.com/q/59364986/3272409. I would appreciate if you could take a look at it. However, thanks for your answer!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.