kcstricks
kcstricks

Reputation: 1569

Swift: Make two types with the same "shape" conform to a common protocol

I have two distinct types that represent the same data, and have the exact same "shape". The two distinct types are code-gen'd and I am forced to deal with them. But, I want to make them conform to a common protocol so that I can treat both types the same. Here's an example:

Let's say that these are my two code-gen'd types, which I am stuck with:

struct User1 {
    var email: String
    var name: Name
    
    struct Name {
        var givenName: String
        var familyName: String
    }
}

struct User2 {
    var email: String
    var name: Name
    
    struct Name {
        var givenName: String
        var familyName: String
    }
}

I want to be able to use these types interchangeably, so I create a couple protocols that they can conform to:

protocol NameRepresenting {
    var givenName: String { get }
    var familyName: String { get }
}

protocol UserRepresenting {
    var email: String { get }
    var name: NameRepresenting { get }
}

And then I attempt to make them conform:

extension User1.Name: NameRepresenting {}
// Error: Type 'User1' does not conform to protocol 'UserRepresenting'
extension User1: UserRepresenting {}

extension User2.Name: NameRepresenting {}
// Error: Type 'User2' does not conform to protocol 'UserRepresenting'
extension User2: UserRepresenting {}

I expect the above to work, but compilation fails with the errors commented above. Is there any elegant way to get these two types to conform to a common protocol so I can use them interchangeably?

Upvotes: 3

Views: 1063

Answers (1)

Sweeper
Sweeper

Reputation: 271355

The name properties of the generated structs have type Name, not NameRepresenting as required by the protocol. Covariant returns are not supported in Swift just yet :(

What you can do is to add an associated type requirement:

protocol UserRepresenting {
    associatedtype Name : NameRepresenting
    var email: String { get }
    var name: Name { get }
}

This requires that the conformers to have a type that conforms to NameRepresenting and is the type of the name property.

However, now that it has an associated type requirement, you cannot use UserRepresenting as the type of a variable/function parameter. You can only use it in generic constraints. So if you have a function that takes a UserRepresenting, you need to write it like this:

func someFunction<UserType: UserRepresenting>(user: UserType) {

}

and if one of your classes/structs need to store a property of type UserRepresenting, you need to make your class/struct generic too:

class Foo<UserType: UserRepresenting> {
    var someUser: UserType?
}

This may or may not work for your situation. If it doesn't, you can write a type eraser:

struct AnyUserRepresenting : UserRepresenting {
    var email: String
    var name: Name
    struct Name : NameRepresenting {
        var givenName: String
        var familyName: String
    }

    init<UserType: UserRepresenting>(_ userRepresenting: UserType) {
        self.name = Name(
            givenName: userRepresenting.name.givenName,
            familyName: userRepresenting.name.familyName)
        self.email = userRepresenting.email
    }
}

Now you can convert any UserRepresenting to this AnyUserRepresenting, and work with AnyUserRepresenting instead.

Upvotes: 3

Related Questions