Skip to content

About

Swipe actions for any SwiftUI view, not just List rows: leading and trailing actions, full swipe and haptics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

SwipeActions: swipe actions for any SwiftUI view, with a phone showing cards swiped open to reveal colored action buttons

CI Swift 6.2 iOS 17+ SwiftUI, any view Swift Package Manager MIT License

SwipeActions adds swipe actions to any SwiftUI view, not just List rows: cards in a ScrollView, rows in a LazyVStack or a plain VStack. Leading and trailing actions, full swipe with a haptic at the threshold, destructive actions that animate the row out, three reveal styles, rounded card buttons, rubber banding, one open row at a time, tap outside to close, right-to-left layouts and VoiceOver actions. No dependencies.

import SwiftUI
import SwipeActions

struct InboxView: View {
    @State private var inbox = Inbox()
    @State private var swipeGroup = SwipeGroup()

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(inbox.mails) { mail in
                    MailCard(mail: mail)
                        .swipeable(id: mail.id) {
                            SwipeAction("Unread", systemImage: "envelope.badge.fill", tint: .blue) {
                                inbox.toggleRead(mail)
                            }
                        } trailing: {
                            SwipeAction("Delete", systemImage: "trash.fill", role: .destructive) {
                                inbox.delete(mail)
                            }
                            SwipeAction("Archive", systemImage: "archivebox.fill", tint: .indigo) {
                                inbox.archive(mail)
                            }
                        }
                }
            }
            .padding(.horizontal)
        }
        .swipeOptions(.cards)
        .swipeGroup(swipeGroup)
    }
}

Screenshots

Captured by CI from the example app on an iPhone simulator. The messages and tasks are a bundled demo dataset written for the example app (generic senders, subjects and to-dos), not real mail. Rows are opened through SwipeGroup.open(_:edge:) and the full swipe is frozen with swipeDebugProgress(_:edge:), so every capture is the same on every run.

Trailing actions Leading actions Full swipe Card list
iPhone: the demo inbox of message cards with the second card slid left, showing rounded Delete, Archive and Flag buttons iPhone: the demo inbox with the first card slid right, showing rounded Read and Pin buttons iPhone: the demo inbox in the middle of a full swipe on the third card, with the red Delete button expanded to fill the uncovered space iPhone: the demo task list in two card sections, Today and Upcoming, with one task slid left showing Delete, Flag and Later buttons sharing the space

Features

  • Any view: a .swipeable { } trailing: { } modifier for cards, custom rows and anything else, in ScrollView, LazyVStack or VStack. It does not clash with SwiftUI's swipeActions.
  • Leading and trailing actions built with if, if/else, switch and for.
  • Full swipe: swiping far enough runs the edge action, with a haptic as you cross the threshold. Per edge, with an adjustable threshold.
  • Destructive role: the row slides out and collapses, then the action runs so you can remove the item.
  • Three reveal styles: .reveal (buttons stay put under the view), .drag (buttons slide in with it) and .expand (buttons share the uncovered space).
  • Custom looks: edge-to-edge rectangles or rounded card buttons with gaps, icon and title, icon only or title only, or a label you draw yourself; corner radius for cards.
  • One row open at a time through a shared SwipeGroup, which also opens and closes rows from code.
  • Tap outside to close: a tap anywhere in the group, or on another row, closes the open row instead of going through.
  • Rubber banding past the buttons and towards edges without actions.
  • Right-to-left: leading and trailing follow the layout direction.
  • Accessibility: every action is a VoiceOver custom action on the row, and Reduce Motion shortens the animations.
  • Scroll friendly: a drag decides once whether it is horizontal, so vertical scrolling keeps working.
  • Tested: the gesture threshold, rubber band, settle and button layout math is pure and covered by Swift Testing.

API

API What it does
.swipeable(leading:trailing:) Adds leading and trailing actions to any view.
.swipeable(id:leading:trailing:) The same, with a stable id so a SwipeGroup can open and close the row from code.
SwipeAction(_:systemImage:tint:role:action:) A button with a title, an optional SF Symbol and a tint (red for destructive, gray otherwise by default).
SwipeAction(_:tint:role:action:label:) A button with a label you draw; the title is still the VoiceOver name.
SwipeActionRole .normal closes the row after the action; .destructive slides the row out and collapses it first.
SwipeActionBuilder The result builder behind the action closures.
SwipeGroup Keeps one row open; open(_:edge:), close(), openRow, isOpen(_:), isAnyRowOpen.
.swipeGroup(_:) Joins the rows inside a container to a group and closes the open row on a tap anywhere in it.
SwipeOptions Style, button width, full swipe edges and threshold, rubber banding, haptics, label style, button shape, corner radius, accessibility and drag distance. .default and .cards presets.
.swipeOptions(_:) Sets the options for every swipeable view inside.
.swipeStyle(_:), .swipeButtonShape(_:), .swipeFullSwipe(_:) Change one option.
SwipeRevealStyle .reveal, .drag, .expand.
SwipeButtonShape .rectangle, .rounded(cornerRadius:spacing:).
SwipeActionLabelStyle .titleAndIcon, .iconOnly, .titleOnly.
SwipeEdge, SwipeEdges .leading and .trailing, as one edge or a set.
.swipeDebugProgress(_:edge:) Freezes a row part of the way open, for previews and screenshots.

Installation

Swift Package Manager

In Xcode, choose File → Add Package Dependencies… and enter:

https://github.com/halilozel1903/SwipeActions

Or add it to Package.swift:

dependencies: [
    .package(url: "https://github.com/halilozel1903/SwipeActions", from: "1.0.0"),
],
targets: [
    .target(name: "YourApp", dependencies: ["SwipeActions"]),
]

Usage

Actions

The first action of an edge is the one closest to the screen edge, and the one a full swipe runs.

TaskRow(task: task)
    .swipeable {
        SwipeAction(task.isDone ? "Undo" : "Done", systemImage: "checkmark", tint: .green) {
            tasks.toggleDone(task)
        }
    } trailing: {
        SwipeAction("Delete", systemImage: "trash.fill", role: .destructive) {
            tasks.delete(task)
        }
        if task.canPostpone {
            SwipeAction("Later", systemImage: "clock.fill", tint: .indigo) {
                tasks.postpone(task)
            }
        }
    }

Only trailing actions? Use .swipeable(trailing: { … }).

Reveal styles

row.swipeStyle(.reveal)   // the buttons stay in place under the view (default)
row.swipeStyle(.drag)     // the buttons slide in attached to the view's edge
row.swipeStyle(.expand)   // the buttons share the uncovered space and grow with it

Full swipe and destructive actions

A full swipe runs the edge's first action once the drag passes fullSwipeThreshold of the row's width (60 % by default, and never less than the open width plus half a button). A haptic plays as the finger crosses it, and the edge action expands to fill the space so the person sees what will happen.

When the action has the .destructive role, the row slides out, collapses, and only then runs the action, so removing the item from your data finishes the animation. If the action keeps the item, the row comes back closed.

var options = SwipeOptions()
options.fullSwipeEdges = .trailing     // only the trailing edge
options.fullSwipeThreshold = 0.7
options.hapticsEnabled = true

content.swipeOptions(options)

One open row, tap outside, and opening from code

@State private var swipeGroup = SwipeGroup()

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemCard(item).swipeable(id: item.id) { … } trailing: { … }
        }
    }
}
.swipeGroup(swipeGroup)

// Later:
swipeGroup.open(item.id, edge: .trailing)   // shows the trailing actions of that row
swipeGroup.close()

Starting a swipe on one row closes the open one. While a row is open, a tap on it or on another swipeable row closes it instead of tapping through, and a tap anywhere else in the container closes it too.

Cards

content.swipeOptions(.cards)

// or tune it:
content.swipeOptions(SwipeOptions(
    style: .reveal,
    labelStyle: .iconOnly,
    buttonShape: .rounded(cornerRadius: 20, spacing: 10),
    cornerRadius: 20
))

The row and its buttons are clipped to cornerRadius. Apply a shadow after .swipeable so it isn't clipped.

Custom labels

SwipeAction("Snooze", tint: .purple) {
    snooze(mail)
} label: {
    VStack {
        Image(systemName: "moon.zzz.fill")
        Text("1 h").font(.caption2.bold())
    }
    .foregroundStyle(.white)
}

Right-to-left

Leading and trailing follow layoutDirection: in Arabic or Hebrew, leading actions are on the right and are uncovered by swiping left. Nothing to configure.

Accessibility

Each action is a VoiceOver custom action on the row (swipe up or down with one finger to pick one, double-tap to run it). By default the row is read as one element; set combinesAccessibilityChildren to false when the row has its own controls that must stay reachable. With Reduce Motion on, the open, close and removal animations are short ease-out curves instead of springs.

Previews and screenshots

#Preview("Full swipe") {
    MailCard(mail: .preview)
        .swipeable { … } trailing: { … }
        .swipeDebugProgress(0.8, edge: .trailing)
}

The row is frozen at 80 % of its width towards the trailing edge, with the edge action expanded, and ignores drags.

How it differs from SwiftUI's swipeActions

SwiftUI swipeActions SwipeActions
Where it works List rows Any view, in ScrollView, LazyVStack or VStack
Button look System rectangles Rectangles, rounded card buttons, custom labels
Reveal styles One Reveal, drag, expand
One open row, open from code No SwipeGroup
Full swipe threshold Fixed Per edge, adjustable

Inside a List, SwiftUI's own modifier is usually the better choice.

Example app

cd Example
xcodegen generate
open SwipeActionsDemo.xcodeproj

Run SwipeActionsDemo on an iPhone simulator. The Inbox tab is a mail-like list of cards in a LazyVStack (switch the reveal style and a right-to-left layout from the toolbar menu); the Tasks tab is a task list in card sections built from plain VStacks. Both use a bundled demo dataset. Launch with -screenshot trailing|leading|fullswipe|cards to open a fixed scene, as CI does for the screenshots above.

Requirements

  • Xcode 26 or later (Swift 6.2 toolchain)
  • iOS 17+ (macOS 14+ for running the tests)

Contributing

Issues and pull requests are welcome. Please run the tests before opening a PR:

swift test
bash scripts/test.sh iOS

License

SwipeActions is available under the MIT license. See LICENSE.

About

Swipe actions for any SwiftUI view, not just List rows: leading and trailing actions, full swipe and haptics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages