SwiftUI Swipe Actions

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

A purple map pin action on trailing edge of row

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) { ... }

Purple map pin action on leading edge of row

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:

Leading actions, green favorite, purple visit

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:

Trailing actions, purple visit, green favorite

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

Trailing red trash can delete action

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.

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

Learn More