I have a view, TimeEditView, that edits an object of an Item class I made. The class does not conform to ObservableObject, since if it does, then it will cause issues with using GRDB.
I am currently using that for my database, since for objects to be compatible with saving to and reading from a GRDB database, their classes must conform to Codable, and using the @Published property wrapper with variables makes the class no longer automatically conform to Codable. Manually adding conformance is something I would like to avoid unless it is absolutely necessary.
Because of this, I don't know how best to be able to bind to / observe the object so that I can edit it and observe its changes.
import SwiftUI
struct TimeEditView: View {
@StateObject var vm = PreviewViewModel()
@ObservedObject var item: Observable<Item>
//@State var item: Item = Item()
@Binding var selectedTime: Date
@State var includesTimes: Bool = false
var body: some View {
NavigationStack {
VStack {
Text(item.name)
TextField("Name", text: $item.name)
Toggle("Time", isOn: $item.includesTimes)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.ultraThinMaterial)
)
if item.includesTimes {
DatePicker("Time", selection: $selectedTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(WheelDatePickerStyle())
.labelsHidden()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.ultraThinMaterial)
)
}
}
.padding()
.navigationTitle("Time")
.navigationBarTitleDisplayMode(.inline)
.presentationDetents([.medium])
.animation(.easeInOut(duration: 0.2), value: item.includesTimes)
}
}
}
The behaviour I am expecting to see is the item.name Text updating when I type into the textfield (that is just a test to ensure the binding is working), and the DatePicker being hidden and shown when I switch the toggle (this is what I actually need to work within my app).
In other views, I have simply bound the object and it seems to have worked okay, updating when I make changes to the item. But when using @State var item: Item here, this does not work. The DatePicker remains hidden when I switch the toggle.
#Preview {
@Previewable @State var item = Item()
@Previewable @State var selectedTime = Date.now
TimeEditView(item: $vm.item, selectedTime: $selectedTime)
}
I then tried using a View Model to store an Item to pass into the Preview as a binding, as this is the approach I will use in the view that contains TimeEditView:
final class PreviewViewModel: ObservableObject {
@Published var item: Item = Item()
}
#Preview {
@Previewable @StateObject var vm = PreviewViewModel()
@Previewable @State var selectedTime = Date.now
TimeEditView(item: $vm.item, selectedTime: $selectedTime)
}
But this didn't work either. Still no updates when changing the name or toggling the switch.
The only thing that did seem to work is using a custom Observable class provided here: How to make a non-ObservableObject observable? and using it with Item.
@dynamicMemberLookup
class Observable<M: AnyObject>: ObservableObject {
var model: M
init(_ model: M) {
self.model = model
}
subscript<T>(dynamicMember kp: WritableKeyPath<M, T>) -> T {
get { model[keyPath: kp] }
set {
self.objectWillChange.send() // signal change on property update
model[keyPath: kp] = newValue
}
}
}
// In TimeEditView
@ObservedObject var item: Observable<Item>
#Preview {
@Previewable @StateObject var item = Observable(Item())
@Previewable @State var selectedTime = Date.now
TimeEditView(item: item, selectedTime: $selectedTime)
}
But I don't really understand how it works, and am not entirely sure if this is the correct way in my case.
Is that the correct way to achieve this? Or is there a better way I should use? I am very confused how I can achieve this as different methods seem to work in different views and I want a common approach I can use across multiple views. I would just pass the parent View Model into DateEditView and access the item from that, but I would like this view to be reusable within other views, so that approach would not work. I'm still pretty new at SwiftUI and Swift so sorry if I am missing something obvious.