Contents

amrita-arun/SwiftMotionKit

Physics-informed swipe interactions for SwiftUI

Features

  • Velocity-based swipe dismissal
  • Distance-based swipe thresholds
  • Smooth spring snap-back
  • Configurable rotation intensity
  • Stack depth layout (vertical stacking)
  • Built-in haptic feedback (rate-limited)
  • Infinite swipe mode (card recycling)
  • Customizable stack styles (vertical, rotated, randomized, etc.)
  • Deterministic randomized layouts
  • Fully SwiftUI-native
  • Swift Package Manager compatible

How Swipe Works

A card dismisses when either:

  • The horizontal drag exceeds the configured distance threshold
  • The predicted end translation exceeds the velocity threshold

Installation

Add SwiftMotionKit via Swift Package Manager:


Then import in your Swift file:

import SwiftMotionKit


---

Quick Start

import SwiftUI
import SwiftMotionKit

struct DemoCard: Identifiable {
    let id = UUID()
    let color: Color
}

struct ContentView: View {
    let cards = [
        DemoCard(color: .pink),
        DemoCard(color: .blue),
        DemoCard(color: .green)
    ]

    var body: some View {
        SwipeCardStack(items: cards) { card in
            RoundedRectangle(cornerRadius: 24)
                .fill(card.color)
                .padding(24)
        }
    }
}

By default:

  • Cards dismiss when dragged beyond 25% of the screen width.
  • Fast flicks also dismiss via velocity detection.
  • Cards snap back smoothly if below threshold.
  • Subtle haptic feedback enhances interaction.

[swipeCardStackResize]

Configuration

You can customize the stack using the initializer:

SwipeCardStack(
    items: cards,
    visibleCardCount: 3,
    swipeThreshold: .percentage(0.25),
    rotationMultiplier: 12,
    releasePreset: .snappy,
    snapBackPreset: .soft,
    velocityThreshold: 500,
    onSwipe: { item, direction in ... },
    stackStyle: .randomized(
        rotationRange: -3...3,
        offsetRange: 10...14
    ),
    recyclingMode: .infinite
) { card in
    CardView(card: card)
}

recyclingMode

Controls whether cards are removed or recycled after being dismissed.

recyclingMode: .none       // default
recyclingMode: .infinite   // cards are appended to the bottom

stackStyle

Controls how background cards are visually displayed.

Available styles:

stackStyle: .none
stackStyle: .vertical(offset: 12)
stackStyle: .scaleAndOffset(scaleStep: 0.03, offsetStep: 12)
stackStyle: .rotated(angleStep: 3, offsetStep: 12)
stackStyle: .randomized(rotationRange: -4...4, offsetRange: 8...16)

.none Only the top card is visible.

.vertical Cards are stacked underneath with vertical offset.

.scaleAndOffset Cards shrink slightly and offset vertically (Tinder-style).

.rotated Cards are slightly rotated under the top card.

.randomized Cards receive deterministic random rotation and offset for a more organic feel.

swipeThreshold

Controls the minimum drag distance required for dismissal.

// 25% of container width
swipeThreshold: .percentage(0.25)

// or absolute points
swipeThreshold: .distance(150)

velocityThreshold

velocityThreshold: 500
  • Lower = easier to dismiss by flick
  • Higher = requires stronger flick

rotationMultiplier

Controls rotation while dragging.

rotationMultiplier: 12

Motion presets (releasePreset, snapBackPreset)

.releasePreset: .snappy
.snapBackPreset: .soft
// or custom:
.custom(response: 0.6, dampingFraction: 0.85)

visibleCardCount

How many cards to draw at once.

visibleCardCount: 3

onSwipe callback

SwipeCardStack(
  items: cards,
  onSwipe: { item, direction in
      print("Swiped \(direction)")
  }
) { card in
  CardView(card: card)
}

Use for:

  • Removing the item from your model
  • Sending analytics
  • Triggering side effects / navigation

How dismissal works

A card dismisses when either: 1. Horizontal drag distance exceeds swipeThreshold OR 2. The predicted end translation (from the gesture) exceeds velocityThreshold

This makes both slow drags and fast flicks feel natural.

Haptic feedback

SwiftMotionKit integrates tasteful haptics:

  • Selection haptic when the drag crosses the threshold (fires once per gesture)
  • Success haptic when the card is dismissed

Haptics are rate-limited to avoid spam.

Note: Simulator does not produce vibration — test on a real device.

When to use SwipeCardStack

Ideal for:

  • Recommendation flows (swipe to like/pass)
  • Matching experiences
  • Product selection
  • Visual browsing and exploratory UIs

Not ideal for:

  • Infinite scroll feeds
  • Precision data entry forms
  • High-density lists where users need exact taps

Example: Full customization

SwipeCardStack(
    items: cards,
    visibleCardCount: 2,
    swipeThreshold: .percentage(0.20),
    rotationMultiplier: 18,
    releasePreset: .soft,
    snapBackPreset: .heavy,
    velocityThreshold: 600,
    onSwipe: { item, dir in
        // update model
    }
) { card in
    YourCardView(card: card)
        .frame(width: 320, height: 480)
}

Edge cases & notes

  • When the stack is exhausted, visibleItems becomes empty — manage repopulation or undo in your app logic.
  • Undo is intentionally left to the integrator (reinsert into your source array).
  • Haptics are no-ops on non-UIKit platforms.
  • Avoid placing the stack inside a scrolling container unless you handle gesture priorities.

Example App

An interactive demo is included in /Examples/SwipeDemoApp.

Run the workspace: SwiftMotionKit.xcworkspace

License

MIT

Package Metadata

Repository: amrita-arun/SwiftMotionKit

Homepage: https://amritaarun.dev

Stars: 3

Forks: 0

Open issues: 0

Default branch: main

Primary language: swift

License: MIT

Topics: animation, gesture, haptics, ios, motion, spm, swift, swiftui, swipe, tinder

README: README.md