Add custom List Swipe Actions in SwiftUI | SwiftUI Bootcamp #58
Swipe actions are one of the most powerful interaction patterns in iOS — think swiping a Mail message to archive it, or swiping a Reminder to complete it. iOS 15 added native swipeActions support to SwiftUI, replacing the limited .onDelete with fully customizable buttons on both trailing and leading edges. After this lesson you'll know how to add multiple swipe actions per edge, style them with tints, and control whether a full swipe triggers the first action.
What You'll Learn
- How to use
.swipeActions(edge:allowsFullSwipe:)to add custom buttons toListrows - How to add multiple actions on both the trailing (right) and leading (left) edges of a row
- How
.tint()on a swipe action button sets its background color - The difference between
allowsFullSwipe: trueandallowsFullSwipe: false
Mental Model
Think of swipe actions like a junk drawer behind a sliding panel on each table row. The trailing edge (swipe right-to-left) reveals buttons from right to left — like opening a drawer from the right side. The leading edge (swipe left-to-right) reveals buttons from left to right — like opening a drawer from the left. Each drawer can have multiple items (buttons), each with its own color and label.
allowsFullSwipe: true on the trailing edge is like a "quick throw" gesture — if the user swipes far enough (past a threshold), the first button's action fires automatically without tapping it. This is the Mail "Archive on full swipe" behavior. allowsFullSwipe: false means the user always has to explicitly tap a button — the row never auto-triggers.
Detailed Explanation
.swipeActions(edge:allowsFullSwipe:content:) is a modifier added to iOS 15. It's applied to each row view inside a ForEach (not to the List itself). The edge parameter is .trailing (right side, revealed by swiping left) or .leading (left side, revealed by swiping right). The content closure contains one or more Button views that appear as swipe action buttons.
The buttons appear in the order declared, from the edge inward. For .trailing, the first declared button is closest to the right edge; for .leading, the first button is closest to the left edge. Each button's background color is set with .tint(Color) — this is different from .foregroundColor, which would only change the label text color. Without .tint(), the button uses the system accent color.
allowsFullSwipe: true enables the "full swipe" gesture where the user swipes past a threshold and the row acts as though the first button was tapped. This is a powerful affordance but also potentially destructive if the first action is irreversible. Mail uses it for Archive (reversible); consider whether your first action is safe enough to trigger accidentally.
You can apply .swipeActions to the same row view multiple times — once for .trailing and once for .leading. This is how the sample gets actions on both sides of the same row. The ForEach loop applies these independently to every row in the list.
The commented-out .onDelete(perform: delete) shows the older API. .onDelete is simpler (single swipe-to-delete on trailing) but can't be combined with .swipeActions on the same ForEach. Choose one or the other; if you need .swipeActions, implement delete as one of your buttons.
Code Structure
ListSwipeActionsBootcamp.swift presents a list of four fruit names. Each row has three trailing swipe actions (Archive, Save, Junk) with different tint colors, and one leading swipe action (Share). An empty delete function is included to show how .onDelete would be connected.
Complete Code
ListSwipeActionsBootcamp.swift
import SwiftUI
struct ListSwipeActionsBootcamp: View {
@State var fruits: [String] = [
"apple", "orange", "banana", "peach"
]
var body: some View {
List {
ForEach(fruits, id: \.self) {
Text($0.capitalized) // $0 is the current element in the ForEach shorthand; .capitalized capitalizes the first letter
.swipeActions(edge: .trailing, allowsFullSwipe: true) { // trailing = swipe left to reveal; full swipe triggers Archive
Button("Archive") {
// implement archive logic here
}
.tint(.green) // background color for the Archive button
Button("Save") {
// implement save logic here
}
.tint(.blue) // background color for the Save button
Button("Junk") {
// implement junk/delete logic here
}
.tint(.black) // background color for the Junk button
}
.swipeActions(edge: .leading, allowsFullSwipe: false) { // leading = swipe right to reveal; no full-swipe trigger
Button("Share") {
// implement share logic here
}
.tint(.yellow) // background color for the Share button
}
}
//.onDelete(perform: delete) // older API: adds a single "Delete" swipe on trailing edge; mutually exclusive with swipeActions
}
}
func delete(indexSet: IndexSet) {
// implement deletion from the fruits array here
// e.g.: fruits.remove(atOffsets: indexSet)
}
}
struct ListSwipeActionsBootcamp_Previews: PreviewProvider {
static var previews: some View {
ListSwipeActionsBootcamp()
}
}Code Walkthrough
@State var fruits: [String]— A local array of fruit names. Swipe actions in a real app would typically call view model methods to update a backing data store. Here the array is state directly in the view for simplicity.ForEach(fruits, id: \.self)— Iterates over the strings, using each string as its own identity (id: \.self). For mutable lists where items might be deleted, using a stable unique ID (like a model'sid) is safer — deleting withid: \.selfcan cause index mismatches if duplicate values exist.Text($0.capitalized)—$0is Swift's shorthand for the first closure argument when the parameter is omitted..capitalizedcapitalizes the first letter of the fruit name for cleaner display. This is equivalent to{ fruit in Text(fruit.capitalized) }..swipeActions(edge: .trailing, allowsFullSwipe: true)— Adds three buttons to the row's right-side swipe.allowsFullSwipe: truemeans swiping all the way to the left triggers "Archive" (the first declared button) automatically. The buttons appear left-to-right from the edge: Archive is rightmost (closest to edge), then Save, then Junk..tint(.green)on the Archive button — Sets the background fill color for this specific swipe action tile..tint()applied here colors the action button's entire background, not just the text. The system automatically adjusts text color (white or black) for legibility against the tint..swipeActions(edge: .leading, allowsFullSwipe: false)— Adds one button to the left-side swipe.allowsFullSwipe: falsemeans the user must explicitly tap "Share" — a full right-to-left swipe doesn't auto-trigger it. This is appropriate for Share since it opens a sheet, which shouldn't happen accidentally.Commented-out
.onDelete— Cannot be combined with.swipeActionson the sameForEach. If you need a delete action, add it as a.tint(.red)button inside.swipeActions..onDeleteadds the standard red "Delete" button automatically but is less flexible.
Common Mistakes
Mistake: Applying .swipeActions to the List instead of the row views inside ForEach.swipeActions must be applied to the individual row view (the direct child of ForEach), not to the List or ForEach itself. Applying it to the wrong level produces no visible swipe actions and no error.
Mistake: Using allowsFullSwipe: true on a destructive (non-reversible) first action
If the first action in your trailing .swipeActions is "Delete" and allowsFullSwipe: true, a careless over-swipe permanently deletes the item. Reserve full-swipe for reversible actions like Archive or Mark as Read. For destructive actions, always require an explicit tap.
Mistake: Trying to combine .onDelete and .swipeActions on the same ForEach.onDelete and .swipeActions conflict — you can't use both on the same ForEach. If you need swipe-to-delete functionality alongside other custom actions, implement delete as a button inside .swipeActions(edge: .trailing) with .tint(.red) and call fruits.remove(atOffsets: ...) inside the button action.
Key Takeaways
.swipeActions(edge:allowsFullSwipe:)attaches to individual row views inside aForEach, not to theList— add it directly to the row content view.- Use
.tint()on eachButtoninside.swipeActionsto set the action's background color; add actions on both.trailingand.leadingedges by chaining two.swipeActionsmodifiers. - Set
allowsFullSwipe: trueonly for safe, reversible actions (Archive, Mark as Read) — a full swipe fires the first action automatically, so it should never be destructive without confirmation.
Last updated: June 27, 2026