Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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 }
}
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading