I don’t know if I’m the only one who feels this way, but I hate the way the swipe-to-delete function looks and feels in SwiftUI. In UIKit, after you swipe a cell, the background animates to fill the rest of the cell with destructive red and the cell collapses. This feels like it’s finishing the swipe you started and disposing of that cell. It’s great!


There are two issues I have with SwiftUI’s behavior:
- When you swipe to delete a cell, the cell starts returning back to normal after the swipe, reducing the amount of red. It feels like it’s undoing your action.
- If you delete the bottom cell in a list, it doesn’t animate properly; it stays visible until the cell finishes collapsing, then it suddenly disappears.
For this second issue, I found a relatively easy fix if you’re using a plain-style table, but it comes with a side-effect. If you simply add a blank cell at the end of the list, then that blank cell with cover the cell being deleted, preventing the issue. This can be done by simply adding a blank Text after your ForEach
in your List
.
List {
ForEach(...) {
...
}
Text("")
}
This does, unfortunately have the unwanted side-effect of leaving an empty cell at the end of your list. If you are using a plain-style table, this will only be noticeable when the list is taller than the screen and you scroll to the bottom, but it doesn’t look too bad, and many users might not even notice.

The first issue was a much harder fix, however.
Animating cell deletion
In order to make the cell swipe-to-delete look and feel better, I sought to recreate the animation you see in UIKit. After much trial and error, I got results that I’m reasonably satisfied with. Unfortunately, I haven’t developed a way to easily implement this onto any list, which is the reason this article isn’t a tutorial. I will, though, show the process and results, so hopefully you can find a way to implement it in your own projects.
The basic view
The basic view I was trying to create was a list of strings. Here’s a simple SwiftUI implementation of that:
struct BasicView: View {
@State var items: [String] = [
"One",
"Two",
"Three",
"Four",
"Five",
"Six"
]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
Text("")
}
}
}
Notice the Text("")
at the end to improve the animation for deleting the last cell.
Marking cells for deletion
In order to animate a cell’s deletion, the cell itself needs to know it’s going to be deleted so it can perform an animation. The way I’m going to be marking cells for deletion is by using a set of strings, since strings are the objects I’m displaying in my list. If the set contains a cell’s string, that cell is marked for deletion. If you swipe to delete a cell, it adds that cell’s string to the set. I found it important to use a set here as opposed to an array, as it is much more efficient to check if an object exists in a set than if it exists in an array.
@State var delete = Set<String>()
var body: some View {
List {
...
}.onDelete { indexSet in
self.remove(at: indexSet)
}
Text("")
}
}
func remove(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
delete.insert(items[index])
}
Here we define a State variable to hold the set of cells to delete, and when you swipe a cell, it adds it to that set. This does not actually delete the cell yet. Right now we’re just trying to get the animation in, then we can delete it.
Adding an animated red background
In order to add a red background for when you delete the cell, I used the .listRowBackground
attribute on the Text
in the ForEach. This allows you to use any view as the background of the cell.
Text(item)
.listRowBackground(
GeometryReader { geo in
Rectangle()
.foregroundColor(Color.red)
.frame(width: geo.size.width * self.delete.contains(item).cgFloat)
.animation(.default)
}
)
The first thing we have in the background view is a GeometryReader
. This allows us to grab info about the size of the view. Inside that is a Rectangle
. This rectangle’s width is set to the width of the GeometryProxy
(the width of the cell), times this term self.delete.contains(item).cgFloat
, which will always be either 0 or 1. This checks if the set contains the current cell. If so, the width of the rectangle should be the full width of the cell. If not, the width should be zero. This depends on an extension of Bool
that I use in a lot of my projects:
extension Bool {
var int: Int { self ? 1 : 0 }
var cgFloat: CGFloat { self ? 1 : 0 }
}
This simply lets you treat a Bool
as 0 for false and 1 for true. Then adding .animation(.default)
means that as soon as the cell’s item
is added to the set, it will the red rectangle filling the cell. Let’s see that in action:

This is looking pretty good, but the rectangle is expanding from the center. Getting it to expand from the right is actually a bit more complicated than I first expected. If you’ve seen tutorials on making a progress bar in SwiftUI, I basically did the same thing as that.
Text(item)
.listRowBackground(
GeometryReader { geo in
ZStack(alignment: .trailing) {
Rectangle()
.frame(width: geo.size.width)
.foregroundColor(Color.clear)
Rectangle()
.foregroundColor(Color.red)
.frame(width: geo.size.width * (self.delete[item] ?? false).cgFloat)
.animation(.default)
}
}
)
Here we’re creating a ZStack
with trailing
alignment to make the rectangle animate from the right where we swipe from. The reason we have the extra, clear rectangle is to give context to our red rectangle. Without it, the ZStack
‘s frame would be the exact same size as our red rectangle, so setting an alignment wouldn’t affect its position at all, since it’s already taking up the entire frame.

Now the animation is looking good! We just need to make it actually delete the cell now.
Deleting the cell
Deleting the cell after performing the animation wasn’t quite as easy as I was hoping, but it wasn’t all that hard either.
We can’t simply add a line to remove the cell’s object from the list in the remove(at: )
function, because then the animation wouldn’t play. What we have to do instead is to add a very slight delay removing the cell so the animation can start first. I did this by using DispatchQueue
to add a 0.1 second delay before removing the cell.
func remove(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
delete.insert(items[index])
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.delete.remove(self.items[index])
self.items.remove(at: index)
}
}
With that, we have what I think is a pretty good swipe-to-delete animation. It’s still not as good as UIKit’s, but it’s much better than before.
