iosswiftgenericsswiftuiparametric-polymorphism

can I pass a generic type into a SwiftUI view as @Bindable?


Swift newbie here, please be kind! How can I convert this SwiftUI view into a version that accepts different types of items as input?

I have this view in my iOS app:

struct EditMeshType: View {
    
    @Bindable var item: MeshType
    private let navTitle: String = "Edit Mesh Types"
    var body: some View {
        Form {
            TextField("Name", text: $item.name)
            Toggle("Active?", isOn: $item.activeFlag)
        }
        .navigationTitle(navTitle)
        .navigationBarTitleDisplayMode(.inline)
    }
}

This view is called from another view, and I pass in a specific "item" of type "MeshType" to allow user to change the name or active flag. The main view uses SwiftData, and "item" is a member of an array of MeshType that is part of a @Query declaration. I have pasted in the MeshType object below.

I want to pass in types other than MeshType to this view. For example, I have another type called ArtifactType that also has "name" and "activeFlag" properties. I have many attributes that follow this pattern. I don't want to create separate views for each one if I can instead create a generic view. This is a simplified example, but hopefully it will suffice to say that I have reasons for not wanting to combine these (similar but not identical) lookups into a single class with a "type" categorizer.

I tried this:

struct EditLookup<T>: View {
    
    @Bindable var item: T
    private let navTitle: String = "Edit Lookup Value"
    var body: some View {
        Form {
            TextField("Name", text: $item.name)
            Toggle("Active?", isOn: $item.activeFlag)
        }
        .navigationTitle(navTitle)
        .navigationBarTitleDisplayMode(.inline)
    }
}

but the @Bindable line gives the compiler error: "'init(wrappedValue:)' is unavailable: The wrapped value must be an object that conforms to Observable"

This confuses me, because MeshType uses the @Model macro, which I thought gave me conformance to Observable. Somehow the generic version doesn't know that whatever type T is conforms to Observable.

So then I tried referencing a protocol for T:

protocol Lookup: Observable {
    var name: String { get set }
    var sortOrder: Int { get set }
    var metaCreateDate: Date { get set }
    var activeFlag: Bool { get set }
    var preventDelete: Bool { get }
    var logentries: [LogEntry]? { get }
}

and conforming my MeshType model type to this protocol, and then editing my generic view to reference the protocol as a constraint:

struct EditLookup<T: Lookup>: View {
    
    @Bindable var item: T
    private let navTitle: String = "Edit Lookup Value"
    var body: some View {
        Form {
            TextField("Name", text: $item.name)
            Toggle("Active?", isOn: $item.activeFlag)
        }
        .navigationTitle(navTitle)
        .navigationBarTitleDisplayMode(.inline)
    }
}

but that gives me the same error in the compiler.

Am I going about this wrongly? is there a better way to achieve my goal (pass in various types to a generic Edit view?)

Here is my MeshType model. MeshType is one of many "lookup" fields on a LogEntry. The other lookup fields have similar structures and they all conform to Lookup.

@Model
class MeshType: Lookup {
    var name: String
    var sortOrder: Int
    var metaCreateDate: Date
    var activeFlag: Bool
    
    @Relationship(inverse: \LogEntry.meshType)
    var logentries: [LogEntry]?
    
    var preventDelete: Bool {
        logentries?.count ?? 0 > 0
    }
    
    init(name: String, sortOrder: Int = 0) {
        self.name = name
        self.sortOrder = sortOrder
        self.metaCreateDate = Date.now
        self.activeFlag = true
    }
    static var sample: [MeshType] {
        [
            .init(name: "Window Screen", sortOrder: 1),
            .init(name: "Hardware Cloth", sortOrder: 2)
        ]
    }
}

Thanks to anyone who can help me out!


Solution

  • The error contains a very important piece of information:

    The wrapped value must be an object

    You need Lookup to conform to AnyObject since @Bindable expects a class. I'm surprised that the Observable protocol doesn't do that already.

    protocol Lookup: Observable, AnyObject {
        var name: String { get set }
        var sortOrder: Int { get set }
        var metaCreateDate: Date { get set }
        var activeFlag: Bool { get set }
        var preventDelete: Bool { get }
    }