Custom protocol does't support Identifiable in SwiftUI

402 views Asked by At

I have defined a CustomProtocol, which requires a unique identifier. I have also created a CustomModel that implements this protocol. Despite having an id property as required by the Identifiable protocol, I am unable to use CustomModel as an identifiable type in SwiftUI.

protocol CustomProtocol: Identifiable {
    var id: String { get }
}

struct CustomModel: CustomProtocol {
    let id = UUID().uuidString
}

class CustomModelStore: ObservableObject {
    @Published var models: [any CustomProtocol] = []
    
    init() {
        models = Array(repeating: CustomModel(), count: 10)
    }
}

struct CustomProtocolView: View {
    
    @StateObject var store = CustomModelStore()
    @State var selectedModel: (any CustomProtocol)?
    
    var body: some View {
        VStack {
            ForEach(store.models) { model in
                Text(model.id)
                    .font(.footnote)
                    .onTapGesture {
                        selectedModel = model
                    }
            }
            .sheet(item: $selectedModel) { model in
                Text(model.id)
                    .font(.subheadline)
            }
        }
    }
}

struct CustomProtocolView_Previews: PreviewProvider {
    static var previews: some View {
        CustomProtocolView()
    }
}

Of course I can specify id in ForEach, but this way doesn't acceptable for me because I have no chance do it in .sheet or .fullScreenCover view modifiers. In my store I also can't change type from CustomProtocol to CustomModel

1

There are 1 answers

0
Rob Napier On

This is the standard "protocol existentials (any types) do not conform to protocols." selectedModel does not conform to CustomProtocol (or to Identifiable). Only concrete types conform to protocols.

As Baglan notes, you can deal with the ForEach by passing id: \.id, but you'll have to redesign the .sheet to not require Identifiable, or you'll need to get rid of the any CustomProtocol and use a concrete type.

Exactly how that works depends a lot on exactly what CustomProtocol does and what kinds of objects are in models. If they're all the same object (like in your example), then you would generally make CustomModelStore and CustomProtocolView generic:

// Make generic over the model
class CustomModelStore<Model: CustomProtocol>: ObservableObject {
    @Published var models: [Model] = []

    // Initialize with the correct type, not hard-coded CustomModel
//    init() {
//        models = Array(repeating: CustomModel(), count: 10)
//    }
}

// Make generic over the model
struct CustomProtocolView<Model: CustomProtocol>: View {

    @StateObject var store = CustomModelStore<Model>()
    @State var selectedModel: Model?

    // Now your original code is fine.
    var body: some View {
        // ...
    }
}

If you have different types in the array, then it will depend on how all of this really works to design the right solution. Generally the answer is to .map the objects to some concrete Model struct that the view uses rather than the underlying objects, but it depends on your problem.