Apple added table row swipe actions to UIKit back in iOS 11. SwiftUI got them in iOS 15, let’s see how to use them.
Swipe Actions Modifier
In SwiftUI, you add swipe actions to a row in a ForEach
list with a view modifier:
var body: some View {
List {
ForEach(countries) { country in
CountryCell(country: country)
.swipeActions() {
Button { store.toggleVisited(country)
} label: {
Label("Toggle visited", systemImage: "mappin.circle")
}
.tint(.purple)
}
}
}
}
Note that you always get the fill
variant of any symbols you use in the swipe action. Change the color from the default gray with a tint modifier.
Leading Or Trailing Edge
By default, swipe actions add themselves to the trailing edge of the row. You can override that by specifying the edge:
.swipeActions(edge: .leading) { ... }
When you have more than one action on the same edge the actions appear in the order you add them starting from the swipe actions edge.
.swipeActions(edge: .leading) {
Button { store.favorite(country)
} label: {
Label("Favorite", systemImage: "heart")
}
.tint(.green)
Button { store.toggleVisited(country)
} label: {
Label("Toggle visited", systemImage: "mappin.circle")
}
.tint(.purple)
}
In this example, the first action listed is the favorite action so it appears nearest the leading edge:
Compare this to the postions when we switch the actions to the trailing edge. The actions switch order to keep the favorite action nearest to the trailing edge:
You can have swipe actions on both the leading and trailing edge by adding more than one swipe action modifier:
CountryCell(country: country)
.swipeActions(edge: .leading) {
Button { store.favorite(country)
} label: {
Label("Favorite", systemImage: "heart")
}
.tint(.green)
}
.swipeActions() {
Button { store.toggleVisited(country)
} label: {
Label("Toggle visited", systemImage: "mappin.circle")
}
.tint(.purple)
}
This puts the favorite action on the leading edge and the visited action on the default trailing edge.
Button Roles
In UIKit, a swipe action has a style of .normal
or .destructive
. You get a similar effect in SwiftUI by setting the role of the button:
.swipeActions() {
Button(role: .destructive) { store.delete(country) } label: {
Label("Delete", systemImage: "trash")
}
}
The destructive button role gives the delete action a default red color. You can still change this with a tint modifier if you want a different color.
Note: Once you add a swipe action to a ForEach
list you lose the automatic delete action that SwiftUI adds when you use the onDelete(perform:)
method. You’ll need to add your own delete swipe action instead.
Allow Full Swipe
By default, a full swipe across the row performs the first action. A leading-to-trailing swipe performs the leading action, a trailing-to-leading swipe performs the trailing action. You can disable this by setting allowsFullSwipe
to false:
.swipeActions(allowsFullSwipe: false) {
Accessibility
The best approach is to include a text label with your button. VoiceOver uses it as the accessibility label for the action:
Button { store.toggleVisited(country)
} label: {
Label("Toggle visited", systemImage: "mappin.circle")
}
I find this preferable to using a plain image with an accessibility label:
Button { store.toggleVisited(country)
} label: {
Image(systemName: "mappin.circle")
}
.accessibility(label: Text("Toggle visited"))
Don’t rely on the default description of the symbol. In this case it would be “Map Pin” which is not helpful to the user.
Navigation Links
One final tip when using navigation links. Apply the swipe actions to the outer NavigationLink
element and not the destination view:
ForEach(countries) { country in
NavigationLink(destination: CountryView(country: country),
tag: country.id,
selection: $selection) {
CountryCell(country: country)
}
.swipeActions(edge: .leading) {
Button { store.favorite(country)
} label: {
Label("Favorite", systemImage: "heart")
}
.tint(.green)
}
}