How to conform protocol to Identifiable

I have

protocol Model: Identifiable where ID == String? {
    var id: Self.ID { get set }
}

struct WidgetModel: Model {
    var id: String?
}

struct ContentView: View {
    let models = [any Model]()
    var body: some View {
        //example 1
        ForEach(models) { _ in
        }
        //example 2
        .sheet(item:model) {
        }
    }
}

but gives compiler errors that

Type 'any Model' cannot conform to 'Identifiable'

According to this discussion the compiler might have a hard time because it doesn't know the type of ID but I specifically told it that it would be String

If this is a swift limitation at this point, how can I work around this? I need to have an array of the protocol because essentially I'm iterating mixed concrete types. I've seen a solution mentioned in that thread where you extend ForEach to get over this but this is not an option because there are plenty of other places where SwiftUI expects an identifiable without allowing you to specify id (.sheet as just one example).

The problem is not that you can’t access the id as a concrete type (String?): e.g. try:

let firstID/*: String?*/ = models[0].id 

The problem here is that the ForEach initializer expects of a collection of an Identifiable-conforming type, but models is an array of any Model. This “any” (“existential”) type, as other “any” types, can’t conform to other protocols. Thus, any Model cannot conform to Identifiable. What you can do instead is create a wrapper type yourself which can conform to Identifiable:

struct AnyModel: Model {
  private var _model: any Model

  init(_ model: some Model) {
    _model = model // Automatically casts to “any” type
  }

  var id: String? {
    get { _model.id }
    set { _model.id = newValue }
  }
}

Then, you can use this wrapper type where you’d use the any type. E.g. models would now have a type [AnyModel]. This is a common strategy in Swift (see AnyHashable) and SwiftUI (see AnyView).

2 Likes

Is this the same?

protocol Model: Identifiable {
    var id: String? { get set }
}

Is this using the new observation machinery or there is no need for observation (e.g. the models are static)?

I wouldn't say that it's a Swift limitation. When ForEach declares that it supports collections with an Identifiable element type, it intends for every element to have the same type. That can be important to ForEach's implementation or the API it wishes to present. It's not necessarily true that it will just work with heterogeneous values.

For example, the Identifiable.id property has semantic requirements - it represents the logical identity of some piece of data. It is reasonable to expect that, within a single type, we can effectively guard against id collisions - e.g. if we had a Person type, we could use Person.socialSecurityNumber to identify each person, and not have to worry about collisions.

But if we have a heterogeneous collection, the space for potential conflicts grows dramatically. Let's imagine we have a social media App where people can share contacts (as People objects), interesting movies, books, and other data -- could a Movie.id ever, by coincidence, have the same value as a Person.id or Book.id? It certainly might!

And so this is why, in this case, I think it is correct to require a manually-written wrapper type. This allows you to specify which kinds of objects can safely live together while abiding by the requirements of the Identifiable protocol. Personally, I would recommend an enum to make it clear which types live in the list together, and to make extracting the data easier:

struct ContentView: View {

  enum Element: Identifiable {
    case contact(Person)
    case movie(Movie)
    case book(Book)
  
    var id: String {
      switch self {
        case .contact(let p): p.id
        case .movie(let m): m.id
        case .book(let b): b.id
      }
    }
  }

  var elements: [Element]
}

If they're is a risk of collisions, this also gives you the opportunity to mix additional data in to the ID to disambiguate. For example, by adding a prefix to each ID, we ensure that Person.id and Movie.id cannot collide:

    var id: String {
      switch self {
        case .contact(let p): "p" + p.id
        case .movie(let m): "m" + m.id
        case .book(let b): "b" + b.id
      }
    }
7 Likes

Technically, not quite:

protocol Model: Identifiable {
    var id: String? { get set }
}


struct S: Model {
    typealias ID = Int

    @_implements(Identifiable, id)
    var id1: Int { 0 }

    @_implements(Model, id)
    var id2: String?
}

let s = S()

(s as any Identifiable).id // 0
(s as any Model).id // nil

This example uses the unofficial @_implements attribute, but the point here is that we end up with separate witness table entries for Model.id and Identifiable.id, they are not required to be backed by the same implementation.

2 Likes

Thank you. Practically speaking when should I use

protocol Model: Identifiable where ID == String? {
    var id: Self.ID { get set }
}

instead of

protocol Model: Identifiable {
    var id: String? { get set }
}

Tangentially, I'm surprised that these are (apparently) considered equivalent by the Swift compiler.

In my mind the latter:

protocol Model: Identifiable {
    var id: String? { get set }
}

…is very clear - Models are Identifiable and their identifiers are optional Strings.

Whereas:

protocol Model: Identifiable where ID == String? {
    var id: Self.ID { get set }
}

…reads to me as Model is a protocol, which is Identifiable iff id is an optional String. So you can have e.g.:

struct OtherModel: Model {
    var id: Int // No conflict because `Model` doesn't imply `Identifiable` since it cannot fit `ID` to `String?`.
}

…but I see from the compiler error that this is not how Swift interprets this. That's a bit unintuitive.

Given the ambiguous meaning of the where-clause using version, I think it should be avoided. Plus it's more verbose anyway.

If one plays around a bit and discovers that protocols cannot be extended, then Swift's behaviour here starts to make a little sense. But it's not apparent why protocols can't be extended, nor if that restriction will always exist. So it's "sensible" only insofar as it seems self-consistent, not justified.

1 Like

Is wrapper the only solution I have around this problem? I don't mind the clumsiness I'm just worried that the wrapper will have other weird side effects as I use these models with SwiftUI and it's @Published logic.

Actually I dont understand how the wrapper is used. Sure now you can pass it to ForEach, but then how do you access the underlying model?

When I ran into this challenge I just switched to the version of ForEach that takes an id parameter

ForEach(models, id: \.id) { _ in }

Your Model protocol doesn’t even have to conform to Identifiable, it just needs a var to use for that id parameter.

That wont work because I'm not looking to fix ForEach, I'm looking to get Identifiable, which is why I included the second example.

I've been trying to go about this another way using generics but I encounter another problem, any ideas how to use this approach?


protocol Model: Identifiable {
    var id: Self.ID { get set }
}

struct WidgetModel: Model {
    var id: String
}

struct ContentView<T:Model>: View {
    @StateObject var vm = VM<T>()
    var body: some View {
        ForEach(vm.models) { _ in
        }
    }
}

class VM<T: Model>: ObservableObject {
    @Published var models = [T]()
}

Error

Generic parameter 'T' could not be inferred

 var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

This is a fundamental difference between generics vs any Model. any Model allows you to mix different models in the same array, while a generic requires them to all be the same model but that model is specified by the caller. You could have:

struct Foo: Model { ... }
struct Bar: Model { ... }

var body: some Scene {
    WindowGroup {
        ContentView<Foo>()
        // OR
        ContentView<Bar>()
    }
}
1 Like

I actually have an array of mixed type. So I guess the only solution is wrapper then right?

If you have a fixed list of types, you could use an enum instead of a protocol:

enum Model: Identifiable {
    case a(A)
    case b(B)
    …

    var id: String? {
        switch self {
        case .a(let a): a.id
        case .b(let b): b.id
        …
        }
    }
}
1 Like

This sounds like an interesting solution, but can you explain how I would get the underlying sub classes?

struct ContentView: View {
    let models = [Model]()
    var body: some View {
        //example 1
        ForEach(models) { model in
               // how to get WidgetModel properties?
        }
        //example 2
        .sheet(item:model) {
        }
    }
}

You would switch over the model:

struct ContentView: View {
    let models = [Model]()
    var body: some View {
        //example 1
        ForEach(models) { model in
            switch model {
            case .widget(let widget):
                WidgetView(widget: widget)
            case …:
            }
        }
        //example 2
        .sheet(item:model) { model in
            switch model {
            case .widget(let widget):
                WidgetSheet(widget: widget)
            case …:
            }
        }
    }
}
2 Likes

Nevermind, jlukas has a better approach

An alternative approach based on inheritance and `AnyView`.
import SwiftUI

class Model: Identifiable {
    var id: String { fatalError() }
    var view: any View { fatalError() }
}

class ModelA: Model {
    override var id: String { "A" }
    var text = "Hello, World"
    override var view: any View {
        ViewA(viewModel: self)
    }
}

class ModelB: Model {
    override var id: String { "B" }
    override var view: any View {
        ViewB(viewModel: self)
    }
}

struct ViewA: View {
    let viewModel: ModelA
    var body: some View {
        Text(viewModel.text)
            .font(.largeTitle)
    }
}

struct ViewB: View {
    let viewModel: ModelB
    var body: some View {
        Color.blue.frame(height: 200)
    }
}

class TheModels: ObservableObject {
    @Published var models: [Model] = [ModelA(), ModelB()]
}

struct TheView: View {
    @StateObject var models = TheModels()
    var body: some View {
        VStack {
            ForEach(models.models) { model in
                AnyView(model.view)
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        TheView()
    }
}

#Preview {
    ContentView()
}

@main struct TheApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
1 Like

Thanks that's an interesting solution but I'm not sure I can use class since these structs are created by firebase's library.

Given the tradeoffs I think I'm going with the wrapper strategy, but even that, I'm not sure how it fully works? Here's my best attempt


protocol Model: Identifiable where ID == String? {
    var id: Self.ID { get set }
}

struct AModel: Model {
    var id: String?
    var name: String
}
struct BModel: Model {
    var id: String?
    var count:Int
}
struct AnyModel: Model {
    private var _model: any Model

    init(_ model: some Model) {
        _model = model // Automatically casts to “any” type
    }

    var id: String? {
        get { _model.id }
        set { _model.id = newValue }
    }
}

struct TestView: View {
    let models = [AnyModel](arrayLiteral: AnyModel(AModel(name: "test")), AnyModel(BModel(count: 3)))
    var body: some View {
        ForEach(models) { model in
             //how to read model.name or model.count?
        }
    }
}
Same as above with structs
import SwiftUI

protocol Model {
    var id: String { get }
    var view: any View { get }
}

struct ModelA: Model {
    var id: String { "A" }
    var text = "Hello, World"
    var view: any View {
        ViewA(viewModel: self)
    }
}

struct ModelB: Model {
    var id: String { "B" }
    var view: any View {
        ViewB(viewModel: self)
    }
}

struct ViewA: View {
    let viewModel: ModelA
    var body: some View {
        Text(viewModel.text)
            .font(.largeTitle)
    }
}

struct ViewB: View {
    let viewModel: ModelB
    var body: some View {
        Color.blue.frame(height: 200)
    }
}

class TheModels: ObservableObject {
    @Published var models: [any Model] = [ModelA(), ModelB()]
}

struct TheView: View {
    @StateObject var models = TheModels()
    var body: some View {
        VStack {
            ForEach(models.models, id: \.id) { model in
                AnyView(model.view)
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        TheView()
    }
}

#Preview {
    ContentView()
}

@main struct TheApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
1 Like

The properties accessed via existential as protocol requirements could be accessed directly. To get to other model specific properties you'd need to cast the model to the appropriate type. Here's how you could do this with switch:

func dumpProperties(_ model: Model) {
    
    // for properties accessible via existential:
    print("id: \(model.id)")
    
    // for the properties distinct to each model:
    switch model {
    case let model as ModelA:
        print("this is modelA \(model.text)")
    case let model as ModelB:
        print("this is modelB \(model.count)")
    default:
        fatalError()
    }
}

or if you prefer ifs:

    if let model as? ModelA {
        print("this is modelA \(model.text)")
    } else if let model = model as? ModelB {
        print("this is modelB \(model.count)")
    } else {
        fatalError()
    }

If you are sure that at some point the model must be of a particular type (otherwise it's a catastrophic error), then:

let model = model as! ModelA