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!
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 }
}