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)
//}