How to handle alert when deleting row from List

This is another issue found after changing to use a @StateObject for my data model when populating a List.

Previous issue is here: https://developer.apple.com/forums/thread/805202 - the entire List was being redrawn when one value changed, and it jumped to the top.

Here's some code:

struct ItemListView: View {
	@State private var showAlert: Bool = false
	...

	fileprivate func drawItemRow(_ item: ItemDetails) -> some View {
		return ItemRow(item: item)
			.id(item.id)
			.swipeActions(edge: .trailing, allowsFullSwipe: false) {
				RightSwipeButtons(showAlert: $showAlert, item: item)
			}
	}
	...

	List {
		ForEach(modelData.filteredItems.filter { !$0.archived }) { item in
			drawItemRow(item)
		}
	}
	...

	.alert("Delete Item"), isPresented: $showAlert) {
		Button("Yes, delete", role: .destructive) {
			deleteItem(item.id)  // Not important how this item.id is gained
		}
		Button("Cancel", role: .cancel) { }
	} message: {
		Text("Are you sure you want to delete this item? You cannot undo this.")
	}
}

struct RightSwipeButtons: View {
	@Binding var showAlert: Bool

	var body: some View {
		Button { showAlert = true } label: { Label("", systemImage: "trash") }
	}
}

The issue I have now is that when you swipe from the right to show the Delete button, and tap it, the alert is displayed but the list has jumped back to the top again. At this point you haven't pressed the delete button on the alert.

Using let _ = Self._printChanges() on both the ItemsListView and the individual ItemRows shows this:

ItemsListView: _showAlert changed.
ItemRow: @self, @identity, _accessibilityDifferentiateWithoutColor changed.

So yeah, that's correct, showAlert did change in ItemsListView, but why does the entire view get redrawn again, and fire me back to the top of the list?

You'll notice that it also says _accessibilityDifferentiateWithoutColor changed on the ItemRows, so I commented out their use to see if they were causing the issue, and... no.

Any ideas?

(Or can someone provide a working example of how to ditch SwiftUI's List and go back to a UITableView...?)

In your code snippet you had:

List {
    ForEach(modelData.filteredItems.filter { !$0.archived }) { item in
        drawItemRow(item)
    }
}

Every time SwiftUI re-renders the view, the closure runs again and you're performing that filtering on every update to the View. Apart from this being not performant, you could have unstable ids that are different every view update.

I'd suggest you either pre-compute your filtered items once in an observable object.

Okay, I've created new vars in the model data. The initial set of data is read from Core Data, which is then filtered into the standardItems var, and the archived ones go into archivedItems.

The List now doesn't use .filter, it just uses the specific set of data as required.

However, this did not fix the issue with the list refreshing when I tap the delete swipe button.

I want to show an alert to ask for confirmation to delete an item before deleting it.

I swipe the row to show the delete button. The moment I tap it, the list refreshes. This happens just because the @State var showAlert has been changed. The confirmation alert does appear, but the list is now at the top (again).

Look at the code I added in the first post above. RightSwipeButtons has showAlert as a Binding var, which is why it changes in ItemListView.

I can't use confirmationDialog(_:isPresented:titleVisibility:actions:message:) on the swipe button because the button disappears and the alert has nothing to anchor itself to.

So, how do I do this? How does anyone do this? It seems every time I make a change to adhere to best practice, SwiftUI throws another obstacle in my way that takes a day or more to figure out - and a lot of refactoring.

UPDATE: I believe I've found a fix.

The issue was partly that the buttons I want in .swipeActions(edge: .leading) { ... } are in a separate View struct called LeftSwipeButtons, and the ones going into .swipeActions(edge: .trailing) { ... } are in their own struct called RightSwipeButtons. This means passing Binding vars into them so that the ItemRow can use them, and send their value back to the view that's got my List in it, i.e.:

struct ItemRow: View {
	@Binding var x: Bool
	@Binding var y: Bool

	var body: some View {
		...
	}
	.swipeActions(edge: .leading) {
		LeftSwipeButtons(x: $x, y: $y ... )
	}
	.swipeActions(edge: .trailing) {
		RightSwipeButtons(x: $x, y: $y ... )  // Has .confirmationDialog inside
		// But also exhibits the issue if I put .confirmationDialog here
	}
	.contextMenu {
		ContextMenu( ... )  // Has .confirmationDialog inside
		// But also exhibits the issue if I put .confirmationDialog here
	}
}

Adding .confirmationDialog inside RightSwipeButtons or inside .swipeActions didn't work because the button disappeared.

It seems that the fix for this was to move all that code back into the ItemRow itself and add .confirmationDialog onto the end.

Same problem with the context menu. I couldn't add .confirmationDialog inside my ContextMenu struct, or add it inside .contextMenu.

Once I moved all the code back into ItemRow it all worked (though ItemRow is now about 450 lines...).

struct ItemRow: View {
	var body: some View {
		...
	}
	.swipeActions(edge: .leading) {
		if(a == 1) { ... }. // etc.
	}
	.swipeActions(edge: .trailing) {
		if(b == 2) { ... }. // etc.
	}
	.contextMenu {
		...
	}
	.confirmationDialog { ... }
}
How to handle alert when deleting row from List
 
 
Q