More problems with Swift's type system

I want to define a set of types like this:

protocol UserProfile {
    var userId: String? { get set }
    // ...
}

// UserProfile also has several extensions

protocol AvatarImageCache: NSObject {
    var imageURL: String? { get set }
    var imageLoadError: Bool { get set }
    var image: Image? { get set }
}

protocol UserProfileWithAvatar: UserProfile, AvatarImageCache, Identifiable where ID == String {
    var id: String { get }
}

extension UserProfileWithAvatar {
    var id: String { userId ?? "" }
}

struct GroupMember {
    let user: UserProfileWithAvatar    // error here
    let addedAt: Date
}

This gives the dreaded error, Protocol 'UserProfileWithAvatar' can only be used as a generic constraint because it has Self or associated type requirements. If I remove the Identifiable clause from UserProfileWithAvatar, that error goes away, so it seems like Identifiable is the source of the problem. But why? I've declared the type of ID in the way XCode recommends, so what's the problem? If I need to redeclare it in the struct, how? Adding a similar where clause there gives another error.

There's more. I also tried moving the Identifiable conformance/inheritance clause to the extension:

extension UserProfileWithAvatar: Identifiable {
    typealias ID = String
    var id: String { userId ?? "" }
}

For some reason I can't use where ID == String in this context, but even after fixing that error, I get a new one: Extension of protocol 'UserProfileWithAvatar' cannot have an inheritance clause. Declaring conformance to other protocols in extensions is usually allowed, so what's special about this one? I've also tried removing AvatarImageCache in case its inheritance of NSObject was clashing with some undocumented part of Identifiable, but that didn't change these errors either.

Perhaps I don't even need to declare conformance to Identifiable anyway? I want it so I can display a SwiftUI ForEach view that iterates over a collection of UserProfileWithAvatar (FWIW the protocol is implemented by two classes, one for the local user and one for remote users). ForEach requires that elements conform to Identifiable. 'Class types' have a default implementation of Identifiable but even that's slightly ambiguous. Do they mean any object instances, or only instances of SomeClass.self? If it's all class objects, that should work for me, because AvatarImageCache forces conformance with NSObject (for the sake of mutability), but I doubt it will be that easy, because Swift always seems to throw obstructions in my path.

If the previous paragraph doesn't hold the solution, I suppose I could move the Identifiable to struct GroupMember, but I'd rather keep that as a private implementation detail.

Your original code compiles fine in Swift 5.7, with one small modification:

struct GroupMember {
    let user: any UserProfileWithAvatar // Adding ‘any’ prefix
   …
}

The “self or associatedtype” restriction was lifted by SE-0309 and SE-0335.

3 Likes

Declaring that one protocol refines another in an extension has never been allowed.

4 Likes

Oh. Do you mean:

extension Something: SomethingElse {
    // ...
}

is only valid if the first Something is a concrete type, and invalid if it's a protocol?

That's good to know. I think I have encountered the any keyword in an article or tutorial, but I forgot about it.

Correct. This is one of the places where Swift using the same syntax for inheritance and protocol conformance is confusing. Foo: Bar is inheritance if both Foo and Bar are either protocols or classes, and it's protocol conformance if Foo is a class and Bar is a protocol (and an error if Foo is a protocol and Bar is a class). You can't inherit in an extension for either classes or protocols.