Here's my attempt at using the hasChanges
property. It sort of combines the two approaches. See what you think – I'm not sure how well it works though.
There is an entity called Item
with three attributes: name: String
, detail: String
, amount: Int16
(for reproducibility). I used the template PersistenceController
struct to inject the managedObjectContext
into the views.
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(sortDescriptors: []) private var items: FetchedResults<Item>
@State private var selectedItem: Item?
var body: some View {
NavigationStack {
List(items) { item in
Button {
selectedItem = item
} label: {
LabeledContent(item.title ?? "No name", value: item.amount, format: .number)
}
}
.sheet(item: $selectedItem, content: EditView.init)
.toolbar {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.title = "New Item"
do {
try viewContext.save()
} catch {
fatalError("Failed to save: \(error.localizedDescription)")
}
}
}
}
struct EditView: View {
@Environment(\.dismiss) private var dismiss
@State private var showingCancelAlert = false
@State private var title: String
@State private var detail: String
@State private var amount: Int
let item: Item
init(item: Item) {
self.item = item
_title = State(wrappedValue: item.title ?? "")
_detail = State(wrappedValue: item.detail ?? "")
_amount = State(wrappedValue: Int(item.amount))
}
var body: some View {
NavigationStack {
Form {
Section {
TextField("Title", text: $title)
.onSubmit {
item.title = title
}
TextField("Description", text: $detail)
.onSubmit {
item.detail = detail
}
}
Stepper("Amount: \(amount)", value: $amount) { _ in
item.amount = Int16(amount)
}
}
.navigationTitle("Edit Item")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", role: .cancel) {
if item.hasChanges {
showingCancelAlert = true
} else {
dismiss()
}
}
.confirmationDialog("You have unsaved changes", isPresented: $showingCancelAlert) {
Button("Discard Changes", role: .destructive, action: dismiss.callAsFunction)
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: dismiss.callAsFunction)
.disabled(!item.hasChanges)
}
}
}
.interactiveDismissDisabled()
}
}