I'm trying out putting most of my business logic in a Protocol
that my @Model
can conform to, but I'm running into a SwiftUI problem with a Binding
that does not get magically offered up like it does when it the subview is not generic.
I have a pretty basic List with a ForEach that now can't properly pass to a generic view based on a protocol. When I try to make a binding manually in the row it says that "item is immutable"... but that also doesn't help me with the NavigationLink? Which is seeing the Binding<Thing> not the <Thing>? But before when the subview was concrete to Thing, it took in the <Thing> and made its own Binding<Thing> once it hit the view. I'm unclear on precisely where the change happens and what I can do to work around it.
Before I go rearchitecting everything... is there a fix to get the NavigationLink to take on the object like before? What needs to be different?
I've tried a number of crazy inits on the subview and they all seem to come back to saying either it can't figure out how to pass the type or I'm trying to use the value before it's been initialized.
Have I characterized the problem correctly?
Thanks!
(let me know if I forgot a piece of code, but this should be the List, the Model/Protocol and the subview)
import SwiftUI import SwiftData struct ThingsView: View { @Environment(\.modelContext) var modelContext @Query var items: [Thing] var body: some View { NavigationStack { List { ForEach(items) { item in NavigationLink(value: item) { VStack(alignment: .leading) { Text(item.textInfo) .font(.headline) Text(item.timestamp.formatted(date: .long, time: .shortened)) } } }.onDelete(perform: deleteItems) } .navigationTitle("Fliiiing!") //PROBLEM HERE: Cannot convert value of type '(Binding<Thing>) -> EditThingableView<Thing>' to expected argument type '(Thing) -> EditThingableView<Thing>' .navigationDestination(for: Thing.self, destination: EditThingableView<Thing>.init) #if os(macOS) .navigationSplitViewColumnWidth(min: 180, ideal: 200) #endif .toolbar { #if os(iOS) ToolbarItem(placement: .navigationBarTrailing) { EditButton() } #endif ToolbarItem { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } ToolbarItem { Button("Add Samples", action: addSamples) } } } } func addSamples() { withAnimation { ItemSDMC.addSamples(context: modelContext) } } private func addItem() { withAnimation { let newItem = ItemSDMC("I did a thing!") modelContext.insert(newItem) } } func deleteItems(_ indexSet:IndexSet) { withAnimation { for index in indexSet { items[index].delete(from: modelContext) } } } } #Preview { ThingsView().modelContainer(for: ItemSDMC.self, inMemory: true) }
import Foundation import SwiftData protocol Thingable:Identifiable { var textInfo:String { get set } var timestamp:Date { get set } } extension Thingable { var thingDisplay:String { "\(textInfo) with \(id) at \(timestamp.formatted(date: .long, time: .shortened))" } } extension Thingable where Self:PersistentModel { var thingDisplayWithID:String { "\(textInfo) with modelID \(self.persistentModelID.id) in \(String(describing: self.persistentModelID.storeIdentifier)) at \(timestamp.formatted(date: .long, time: .shortened))" } } struct ThingLite:Thingable, Codable, Sendable { var textInfo: String var timestamp: Date var id: Int } @Model final class Thing:Thingable { //using this default value requires writng some clean up logic looking for empty text info. var textInfo:String = "" //using this default value would require writing some data clean up functions looking for out of bound dates. var timestamp:Date = Date.distantPast init(textInfo: String, timestamp: Date) { self.textInfo = textInfo self.timestamp = timestamp } } extension Thing { var LiteThing:ThingLite { ThingLite(textInfo: textInfo, timestamp: timestamp, id: persistentModelID.hashValue) } }
import SwiftUI struct EditThingableView<DisplayItemType:Thingable>: View { @Binding var thingHolder: DisplayItemType var body: some View { VStack { Text(thingHolder.thingDisplay) Form { TextField("text", text:$thingHolder.textInfo) DatePicker("Date", selection: $thingHolder.timestamp) } } #if os(iOS) .navigationTitle("Edit Item") .navigationBarTitleDisplayMode(.inline) #endif } } //NOTE: First sign of trouble //#Preview { // @Previewable var myItem = Thing(textInfo: "Example Item for Preview", timestamp:Date()) // EditThingableView<Thing>(thingHolder: myItem) //}