Canceling an Edit: When to alert?

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.

how do you know if there are no changes

A simple way. Define a var hasSomeChanges = false

And set to true when changing anything.

I believe NSManagedObject has a hasChanges property so you can check that boolean value, however this requires you to update the object directly whenever the user changes something.

The other option would be doing what @Claude31 suggested, or manually checking each property for a change between the current stored value and the one the user is modifying.

Accepted Answer

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

Got it working in my code with Gene's answer on this Stack Overflow question (https://stackoverflow.com/questions/57614564/swiftui-use-binding-with-core-data-nsmanagedobject); however, it broke my CloudKit syncing.

Thanks everyone. With all the examples I got something working :)

I ended up using the coreData entity and set most controls to the entity's property. This way I could use the ViewContext.hasChanges to determine if I need to ask a user before canceling the form. I had issues with the Toggle and Picker as they wouldn't be set/changed in the edit form but when left the value was updated. I ended up using @BabyJ second post (thanks for the code) to extract a temp variable from the Entity onAppear and set the value onChange. This fixed the controls!

Canceling an Edit: When to alert?
 
 
Q