During one of the WWDC talks this year I remember hearing that when a user clicks cancel on an edit form you should show an alert warning them about discarding the changes before leaving. YET if there aren't any changes we should just exit when canceled.
Looking back at that I wonder how do you know if there are no changes? Right now I load the Core Data Entity fields on my page and set them one by one on save. Is there a better way that would also make it easier to know when to leave and when to warn when canceling? Are there any examples of this?
Thanks everyone.
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()
}
}