Need help with confusing protocol conformance errors

In trying to make a view that can show a table of listings that can show items with the same elements (just a name so far), I've gotten myself stuck in a corner of Swift that I thought I understood but apparently don't.

I created a protocol, NamedModel, that is marked as Identifiable, Hashable and Equatable.

protocol NamedModel : Identifiable, Equatable, Hashable {
     var name: String { get }
     var id: UUID { get }
 }

extension NamedModel {
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id &&
        lhs.name == rhs.name
    }
}

One model type I created is the AppModel:


struct AppModel: NamedModel {
    var name: String
    var id = UUID()
}

I created a protocol for my view models to conform to that includes a property that's an array of NamedModel:

protocol ListViewDataProvider {
    var items: [any NamedModel] { get set }
     ...
}

I created a view model class for my Applications view that conforms to the ListViewDataProvider protocol.

@Observable class ApplicationsListModel: ObservableObject, ListViewDataProvider {
    var items: [any NamedModel] = []
     ...
}

In my view, I have a property, dataProvider, that conforms to that ListViewDataProvider protocol, and uses its items var for populating the table:

struct ApplicationsListView: View {
    var dataProvider: ListViewDataProvider
    @State private var selectedAppID: UUID? = nil
     ...

            Table(dataProvider.items, selection: $selectedAppID) {
                TableColumn("Applications") { app in
                    Text(app.name)
                }
                ...
       }
}

On the line declaring the TableColumn, I get an error: Type 'TableForEachContent<[any NamedModel]>.TableRowValue' (aka 'any NamedModel') cannot conform to 'Identifiable'

I don't understand why this is occurring, as I've marked the NamedModel type as conforming to Identifiable. I'm hoping somebody can help me understand what I'm doing wrong. I'd like to be able to reuse some views with common characteristics in this app.

Is this array actually heterogeneous? If not it's better to use an associated type:

protocol ListViewDataProvider {
    associatedtype Item: NamedModel
    var items: [Item] { get set }
     ...
}

Then in the conforming type, you witness var items with an array of concrete type.

1 Like

Then in the conforming type, you witness var items with an array of concrete type.

If I understand your suggestion correctly, I'm still doing something wrong.

@Observable class ApplicationsListModel: ObservableObject, ListViewDataProvider {
    var items: [AppModel] = []

This yields an error message stating: Type 'ApplicationsListModel' does not conform to protocol 'ListViewDataProvider'

The following code (from original) compiles for me:

import Foundation

protocol NamedModel : Identifiable, Equatable, Hashable {
     var name: String { get }
     var id: UUID { get }
 }

extension NamedModel {
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id &&
        lhs.name == rhs.name
    }
}

struct AppModel: NamedModel {
    var name: String
    var id = UUID()
}

protocol ListViewDataProvider {
    associatedtype Item: NamedModel
    var items: [Item] { get set }
}

@Observable class ApplicationsListModel: ObservableObject, ListViewDataProvider {
    var items: [AppModel] = []
}

As an aside, why conform to ObservableObject? The class is already annotated with Observable.

@Observable class ApplicationsListModel: ObservableObject, ListViewDataProvider {
...

Thanks for the tweak on ListViewDataProvider. I can get your code to compile here as well. But when I try to use the ListViewDataProvider's items property for a table in a view, I still get the error about not conforming to Identifiable.

import SwiftUI
import Observation

struct ApplicationsListView: View {
    var dataProvider: any ListViewDataProvider
    @State private var selectedAppID: UUID? = nil

   var body: some View {
       Table(dataProvider.items, selection: $selectedAppID) {
           TableColumn("Applications") { app in
               Text(app.name)
           }
           
       }
    }
}

Type 'TableForEachContent<[any NamedModel]>.TableRowValue' (aka 'any NamedModel') cannot conform to 'Identifiable'

The ObservableObject annotation was from an earlier implementation. I have now deleted that. Thanks for bringing it to my attention.

Try making this generic:

struct ApplicationsListView<Provider: ListViewDataProvider>: View {
    var dataProvider: Provider

Just tried that, with struct AppModel --> class AppModel:

Details
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, tiny primes world!")
            
            ApplicationsListView (dataProvider: TinyPrimesStore ())
        }
        .padding()
    }
}

protocol NamedModel : Identifiable, Equatable, Hashable {
     var name: String { get }
 }

extension NamedModel {
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id &&
        lhs.name == rhs.name
    }
}

class AppModel: NamedModel {
    var name: String = "<NotSet>"
}

extension AppModel: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

protocol ListViewDataProvider {
    associatedtype Item: NamedModel
    var items: [Item] { get set }
}

#if false
struct ApplicationsListView <Provider: ListViewDataProvider>: View {
    var dataProvider: Provider
    @State private var selectedAppID: UUID? = nil

   var body: some View {
       List (selection: $selectedAppID) {
           ForEach (dataProvider.items, id: \.self) {u in
               Text (u.name)
           }
       }
    }
}
#else
struct ApplicationsListView<Provider: ListViewDataProvider>: View {
    var dataProvider: Provider
    @State private var selectedAppID: UUID? = nil

   var body: some View {
       Table (dataProvider.items, selection: $selectedAppID) {
           TableColumn("Applications") { app in
               Text(app.name)
           }
       }
    }
}
#endif

class TinyPrimesStore: ListViewDataProvider {
    var items: [SmallPrime] = [SmallPrime (2), SmallPrime (3), SmallPrime (5), SmallPrime (7)]
}

class SmallPrime: NamedModel {
    let value: Int
    let name : String
    init (_ value: Int) {
        let uv = [2, 3, 5, 7, 11, 13, 17, 19]
        assert (uv.contains (value))
        self.value = value
        self.name = "Small prime: \(value)"
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

but now getting these errors:

No exact matches in call to initializer 
- Found candidate with type 'UUID?' (SwiftUI.Table.init)
- Found candidate with type 'Binding<UUID?>' (SwiftUI.Table.init)

However, the errors go away if Table...TableColumn construct is replaced with a List...ForEach construct.

Edit (Use the correct type for the table row selection)

New Details
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, tiny primes world!")
            
            ApplicationsListView (dataProvider: TinyPrimesStore ())
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

protocol NamedModel : Identifiable, Equatable, Hashable {
     var name: String { get }
 }

extension NamedModel {
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id &&
        lhs.name == rhs.name
    }
}

class AppModel: NamedModel {
    var name: String = "<NotSet>"
}

extension AppModel: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

protocol ListViewDataProvider {
    associatedtype Item: NamedModel
    var items: [Item] { get set }
}

#if false
struct ApplicationsListView <Provider: ListViewDataProvider>: View {
    var dataProvider: Provider
    @State private var selectedAppID: Provider.Item.ID? = nil

   var body: some View {
       List (selection: $selectedAppID) {
           ForEach (dataProvider.items, id: \.self) {u in
               Text (u.name)
           }
       }
    }
}
#else
struct ApplicationsListView<Provider: ListViewDataProvider>: View {
    var dataProvider: Provider
    @State private var selectedAppID: Provider.Item.ID? = nil

   var body: some View {
       Table (dataProvider.items, selection: $selectedAppID) {
           TableColumn("Applications") { app in
               Text(app.name)
           }
       }
    }
}
#endif

class TinyPrimesStore: ListViewDataProvider {
    var items: [SmallPrime] = [SmallPrime (2), SmallPrime (3), SmallPrime (5), SmallPrime (7)]
}

class SmallPrime: NamedModel {
    let value: Int
    let name : String
    init (_ value: Int) {
        let uv = [2, 3, 5, 7, 11, 13, 17, 19]
        assert (uv.contains (value))
        self.value = value
        self.name = "Small prime: \(value)"
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

Your selectedAppID is not using the following. (Your UUID is not a match.)

@State private var selectedAppID: Provider.Item.ID? = nil
1 Like

Thank you, @Danny.

I am sure @birchdev will be quite pleased.

I will update my post to use the correct type for the table row selection.

PS: I wish the compiler were able to directly point out that there was a type mismatch on that line. @Slava_Pestov

1 Like

@ibex10: Unfortunately I want to use a Table and TableColumns for consistency with other parts of the app.

@Slava_Pestov: I implemented your suggestion for making the view generic and things improved, but I now get an error on the Table instantiation line: No exact matches in call to initializer

Removing the selection argument allows the project to compile and displays the defined rows, but breaks the functionality I need.

Here is the entirety of the code in my test project.

"Models" file:

import Foundation
import Observation

protocol NamedModel : Identifiable, Hashable {
     var name: String { get }
     var id: UUID { get }
 }

extension NamedModel {
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id == rhs.id &&
        lhs.name == rhs.name
    }
}

struct AppModel: NamedModel {
    var name: String
    var id = UUID()
}

protocol ListViewDataProvider {
    associatedtype Item: NamedModel
    var items: [Item] { get set }
}

@Observable class ApplicationsListModel: ListViewDataProvider {
    var items: [AppModel] = []
    
    init() {
        items = [
            AppModel(name: "Curley"),
            AppModel(name: "Moe"),
            AppModel(name: "Shep")
        ]
    }
}

View file:

import SwiftUI
import Observation

struct TableListView<DataProvider: ListViewDataProvider>: View {
    var dataProvider: DataProvider
    @State private var selectedAppID: UUID? = nil

   var body: some View {
       Table(dataProvider.items, selection: $selectedAppID) {
           TableColumn("Applications") { app in
               Text(app.name)
           }
       }
    }
}


#Preview {
    TableListView(dataProvider: ApplicationsListModel())
}

Alternatively, and what I'd recommend:

protocol NamedModel: Identifiable<UUID>, Hashable {
  var name: String { get }
}

@Danny: Thanks for redirecting me to your earlier comment. I had tried a variation of that earlier which failed to fix the problem, so I neglected to apply that to my test project. Your implementation allows me to compile the test project.

Unfortunately, adding functionality from my real project leads to another problem.

.preference(key: ApplicationSelectionPreferenceKey.self, value: selectedAppID?.uuidString ?? "")

results in the error message: Value of type 'DataProvider.Item.ID' has no member 'uuidString'

None of my attempts to rectify that have been successful, so I'm posting it here in case somebody can help me with this issue.

It seems like you read my post before I added the recommended alternative.

My original attempt at accessing the data provider ID came from reading more of the documentation.

I figured out a solution for this that seems reasonable:

        .preference(key: ApplicationSelectionPreferenceKey.self, value: (selectedAppID as? UUID)?.uuidString ?? "")

No, conditionally casting to a type that you know something is, is not reasonable.

But the compiler doesn't seem to know that the ID type is a UUID, or at least that it has a uuidString member.

Sorry, I missed that part of your earlier comment. Thanks so much for the help with this. Unfortunately Generics are more confusing to me than just about anything else in Swift.

1 Like

This problem is not as easy to solve as it should be, because neither the compiler nor the documentation does anything to help. I'm glad that you got through it, and I'm not glad that the people in charge of the cash flow don't care enough about you to have made it easier on you.