0

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

malhal
  • 26,330
  • 7
  • 115
  • 133
Nizami
  • 728
  • 1
  • 6
  • 24
  • 1
    "because I have no chance do it in `.sheet` or `.fullScreenCover` view modifiers" Then why did you show `ForEach`? Wouldn't showing an example with `.sheet` better illustrate the problem? Please show some code that *actually* shows where your problem is. – Sweeper May 16 '23 at 05:58
  • Remove the variable `id` from `CustomProtocol`: it is not an implementation, but just a protocol; because it conforms to `Identifiable`, `id` is implicitly required, only `CustomModel` needs to implement an `id` variable. Maybe that's the problem. – HunterLion May 16 '23 at 06:37
  • @Sweeper Does it make sense? models should acts like `Identifiable` with confirming custom protocol anywhere, without additional options or logic. Anyway updated example with `.sheet` – Nizami May 16 '23 at 06:39
  • I've noticed that as well, you can work around it by specifying the id explicitly like so: `ForEach(viewModel.models, id: \.id)`. – Baglan May 16 '23 at 07:05
  • SwiftUI requires concrete types there is no way around this – lorem ipsum May 16 '23 at 11:56
  • 2
    This really sounds like you want to use `Generics` that conform to your custom protocol, but you can't use a `Protocol` as a `Type`. `Protocols` describe attributes that a `Type` must implement(if any), but they are not in and of themselves a `Type`. – Yrb May 16 '23 at 15:22

1 Answers1

1

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610