How to conform protocol to Identifiable

@tera can you show me how this is used in my real example? You can't just dumpProperties of a private underlying model. Compile error here


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 {
    func dumpProperties(_ model: any 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()
        }
    }
    
    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?
        }
    }
}

Just correct a few names:

        case let model as AModel:
            print("this is modelA \(model.name)")
        case let model as BModel:
            print("this is modelB \(model.count)")

No that doesn't help the fact you are switching on the wrapper, not the underlying model. See this updated model, it crashes


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 {
    func dumpProperties(_ model: any Model) {
        // for properties accessible via existential:
        print("id: \(model.id)")

        // for the properties distinct to each model:
        switch model {
        case let model as AModel:
            print("this is modelA \(model.name)")
        case let model as BModel:
            print("this is modelB \(model.count)")
        default:
            fatalError()
        }
    }
    
    let models = [AnyModel](arrayLiteral: AnyModel(AModel(name: "test")), AnyModel(BModel(count: 3)))
    var body: some View {
        ForEach(models) { anyModel in
            // how to read model.name or model.count?
            let _ = dumpProperties(anyModel)
        }
    }
}

My dumpProperties just expects any Model, not AnyModel. The fix is simple:

        ForEach(models) { anyModel in
            let _ = dumpProperties(anyModel._model)
            Text("Hello")
        }

and remove private:

/*private*/ var _model: any Model

I wonder though why do you want a wrapper. This approach doesn't, and it doesn't need a switch.

I believe you can do it with just one of 1. type erasure (like AnyView) or 2. switch. But why have both at once?

Ok so basically you have to make the underlying model public which is typically not the design for _variables right? I guess that's the only way.

And no, your other design doesn't work because this whole topic is about getting the models to actually literally conform to identifiable. Not just exposing and id, which is what your alternative solution does. It's actually important to conform to identifiable because SwiftUI has ton of APIs which expects an identifiable where you can't just provide and id like you can in ForEach. See my example 2 in original post.

If you have a solution that doesnt use a wrapper that would be preferred as it's probably quite annoying to unwrap each time a model is accessed.

This is the final working solution I settled with, if anyone has any optimization suggestion I would love to hear it.


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 {
    var innerModel: any Model

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

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

struct TestView: View {
    @State var sheetData: AnyModel?
    let models = [AnyModel](arrayLiteral: AnyModel(AModel(id: "1", name: "test")), AnyModel(BModel(id: "2", count: 3)))
    var body: some View {
        VStack {
            ForEach(models) { anyModel in
                switch anyModel.innerModel {
                case let a as AModel:
                    Text("\(a.name)")
                case let b as BModel:
                    Text("\(b.count)")
                default:
                    Text("ERROR")
                }
            }
        }
        .sheet(item: $sheetData) { _ in
            Text("hi")
        }
    }
}
A combination of the above ideas
import SwiftUI

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

struct AModel: Model {
    var id: String?
    var name: String
    var view: any View {
        AView(model: self)
    }
}

struct BModel: Model {
    var id: String?
    var count: Int
    var view: any View {
        BView(model: self)
    }
}

struct AView: View {
    let model: AModel
    var body: some View {
        Text("\(model.name)")
    }
}

struct BView: View {
    let model: BModel
    var body: some View {
        Text("\(model.count)")
    }
}

struct AnyModel: Model {
    var innerModel: any Model

    init(_ model: any Model) {
        innerModel = model
    }
    var id: String? {
        get { innerModel.id }
        set { innerModel.id = newValue }
    }
    var view: any View {
        innerModel.view
    }
}

struct TestView: View {
    @State var sheetData: AnyModel?
    let models: [AnyModel] = [AnyModel(AModel(id: "1", name: "test")), AnyModel(BModel(id: "2", count: 3))]
    var body: some View {
        VStack {
            ForEach(models) { model in
                AnyView(model.view)
            }
        }
        .sheet(item: $sheetData) { _ in
            Text("hi")
        }
    }
}
struct ContentView: View {
    var body: some View {
        TestView()
    }
}

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

  • Using a wrapper AnyModel for Identifiable conformance
  • getting rid of switch
  • separate views for separate models
  • models providing relevant views
  • using type erased AnyView

I'd also check how this works with mutations (unless your models don't change): I recon there'd need to be @ObservableObject / @ObservedObject / @Published or their analogues from the new observation machinery.

1 Like

ok I started refactoring and it seems I ran into a roadblock when I try to use binding.

to extend from your example

class VM:ObservableObject {
   @Published var vmModel:AnyModel = AnyModel(AModel(id: "1", name: "test"))
}

struct TestView: View {
    @State var sheetData: AnyModel?
    @StateObject var vm = VM()
    let models: [AnyModel] = [AnyModel(AModel(id: "1", name: "test")), AnyModel(BModel(id: "2", count: 3))]
    var body: some View {
        VStack {
            ForEach(models) { model in
                AnyView(model.view)
            }
            TextField(text: ???)   //how can I get a binding of AModel?
        }
        .sheet(item: $sheetData) { _ in
            Text("hi")
        }
    }
}

That'd be indeed a bit awkward in this setup:

            TextField("TextField", text: .init(get: {
                let m = vm.vmModel.innerModel as! AModel
                return m.name
            }, set: { newValue in
                var m = vm.vmModel.innerModel as! AModel
                m.name = newValue
                vm.vmModel.innerModel = m
            }))

A few of these and it'd start feeling you are fighting the system.