Swipy is a SwiftUI library for creating swipe actions. It allows you to easily add swipeable actions to your SwiftUI views, similar to the swipe actions in iOS Mail and other apps. 🥳
Make swipe actions easily.
SwiftUI has swipe actions for only items in the built-in List
view and List
is unusable and extremely buggy when you want to make even a bit cute feature-rich UIs. 😞
First, after discovering that List
is extremely buggy, I found a swipe actions library but it was not working in a ScrollView
and then i decided to make Swipy
. Swipy is clean and simple and tested on latest iOS SDK and Swift 6; it supports min iOS 15 and Swift 5. 🥳
- Swipe to reveal actions
- Customizable
- Clean and simple API
- Easy integration with existing SwiftUI views
- UX friendly
- Can be used in a
ScrollView
Swipy supports minimum iOS 15.
Caution
Swipy's isSwipingAnItem
binding is useful with ScrollView
's .scrollDisabled(_ disabled: Bool)
modifier but this modifier requires minimum iOS 16. However, you still have isSwipingANItem
so, you can avoid scrolling if somehow it is true
.
You can install Swipy via Swift Package Manager.
- In Xcode, File > Add Package Dependencies.
- Enter Swipy GitHub repo address
https://github.com/rohanrhu/Swipy
into search bar. - Add Swipy to your project. 🥳
Add Swipy as a dependency in your Package.swift
file:
dependencies: [
// ...
.package(url: "https://github.com/rohanrhu/Swipy", from: "1.0.0")
]
To use Swipy in your project, simply copy the Swipy.swift
file into your project.
Swipy is easy. It has two main views: Swipy
and SwipyAction
.
In most cases, you'll want to make something swipeable in a ScrollView
. First, you need to define a isSwipingAnItem
state.
We always need to have a isSwipingAnItem
state and bind it to all Swipy
sub-views.
Important
If your Swipy
views are inside a ScrollView
(or any kind of scrollables), your Swipy
views will set your isSwipingAnItem
state by bindings to make you able to disable/enable container's scroll properly.
@State private var isSwipingAnItem = false
and you'll use this for your ScrollView
's .scrollDisabled(_ disabled: Bool)
modifier.
To make a view swipeable, you need to wrap it in a Swipy
and provide the isSwipingAnItem
binding.
Swipy(isSwipingAnItem: $isSwipingAnItem) { model in
Text("Swipe me!")
}
You can add actions to a swipeable view by providing a closure that returns a view for each action.
Swipy(isSwipingAnItem: $isSwipingAnItem) { model in
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
SwipyModel
is a model that is passed to the SwipyAction
's content closure. It provides information about the current swipe state.
swipeOffset
: The current swipe offset.isSwiping
: A boolean that indicates whether an item is being swiped.isScrolling
: A boolean that indicates whether the container is scrolling.isSwiped
: A boolean that indicates whether an item is swiped.swipeActionsWidth
: The width of the swipe actions view.contentSize
: The size of the content view.swipeActionsMargin
: The margin for swipe actions.swipeThreshold
: The swipe threshold calculator function.swipeBehavior
: The behavior for swipe actions.scrollBehavior
: The behavior for scroll actions.
swipe()
: Swipes the item.unswipe()
: Unswipes the item.
Note
You can use SwipyModel
to unswipe your item after an action is performed.
You can unswipe your item after an action is performed by calling the unswipe()
method on the SwipyModel
.
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
You can swipe an item imperatively by calling the swipe()
method on the SwipyModel
.
Swipy(isSwipingAnItem: $isSwipingAnItem) { model in
Button {
model.swipe()
} label: {
Text("Swipe me!")
}
} actions: {
SwipyAction { model in
Button {
model.swipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
Swipy has customizations to make it fit your app's design and provide best user experience.
Swipy
takes the following arguments and provide a SwipyModel
to its sub-views.
It takes these arguments:
isSwipingAnItem
: A binding that indicates whether an item is being swiped.content
: A closure that returns the view to be swiped.actions
: A closure that returns a view for each action.- (Optional)
SwipyActionsMargin
: The margin for swipe actions, aSwipyHorizontalMargin
struct. - (Optional)
swipeThreshold
: The swipe threshold calculator function. - (Optional)
swipeBehavior
: The behavior for swipe actions, aSwipySwipeBehavior
struct. - (Optional)
scrollBehavior
: The behavior for scroll actions, aSwipyScrollBehavior
struct.
leading
: The margin on the leading side of the swipe actions.trailing
: The margin on the trailing side of the swipe actions.
Note
Default: SwipyHorizontalMargin(leading: 0, trailing: 0)
Function that calculates the swipe threshold. The function takes a SwipyModel
and returns a Double
.
You'll most likely want to use calculate the swipe threshold based on the width of the swipe actions view that is a property in the SwipyModel
. (\.SwipyActionsWidth
)
Note
Default: \.SwipyActionsWidth
The behavior for swipe actions. You can use predefined behaviors or create custom ones.
The behavior for scroll actions. You can use predefined behaviors or create custom ones.
SwipyAction
takes these arguments:
content(SwipyModel)
: A closure that returns the view for the action.
SwipyAction
provides one argument to your sub-view body SwipyModel
. You'll most likely want to use this to unswipe your item after an action is performed.
Swipy provides customizable behaviors for both swipe and scroll actions. These behaviors can be passed to the Swipy
view to control how swipe and scroll actions are handled.
Important
If you want to implement custom behaviors, it is simple but to avoid misunderstandings on what they are, please read the following carefully.
The SwipySwipeBehavior
struct defines the behavior for swipe actions.
Important
Swipe behavior decider function is for deciding to start or not to start swiping session. Your swipe behavior must return false
to prevent to start swiping.
You can use predefined behaviors or create custom ones.
Important
All the default behaviors ignores swipe behavior the current ongoing user drag session after it is started once until it ends; which means .custom { model, gesture in !model.isSwiped && !model.isSwiping /* ... */ }
.
SwipySwipeBehavior.normal
: Default behavior.SwipySwipeBehavior.soft
: Softer swipe behavior.SwipySwipeBehavior.hard
: Harder swipe behavior.SwipySwipeBehavior.straight
: No guard behavior.SwipySwipeBehavior.disabled
: Disables swipe actions.SwipySwipeBehavior.swiping()
: Behavior when an item is being swiped.SwipySwipeBehavior.swiped()
: Behavior when an item is swiped.SwipySwipeBehavior.offset(_ offset: Double)
: Behavior based on the swipe offset.SwipySwipeBehavior.velocity(_ velocity: Double)
: Behavior based on the swipe velocity.
Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
swipeBehavior: .normal
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
You can combine swipe behaviors using the and
, or
and not
methods.
Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
swipeBehavior: .normal.and(.velocity(400)))
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
You can create custom swipe starter behaviors by providing a custom decider function.
Custom Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
swipeBehavior: .custom { model, gesture in
!model.isSwiped && !model.isSwiping && gesture.translation.width > -100
}
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
High Velocity and Offset Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
swipeBehavior: .custom().offset(200).and(.velocity(2000))
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
The SwipyScrollBehavior
struct defines the behavior for prevention of swiping when user is scrolling vertically.
Important
Scroll behavior is a guard to prevent the container's vertical scroll while swiping. Your scroll behavior must return true
for starting to prevent swiping for the current ongoing user drag session.
SwipyScrollBehavior.normal
: Default behavior.SwipyScrollBehavior.soft
: Softer scroll behavior.SwipyScrollBehavior.hard
: Harder scroll behavior.SwipyScrollBehavior.disabled
: Disables scroll actions.
Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
scrollBehavior: .normal
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
You can create custom scroll behaviors by providing a custom decider function.
Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
scrollBehavior: .custom { model, gesture in
!model.isSwiped && abs(gesture.translation.height) > 10
}
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
You can combine scroll behaviors using the and
, or
and not
methods.
Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
scrollBehavior: .normal.or(.soft).and(.not(.hard))
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
Another Example:
Swipy(
isSwipingAnItem: $isSwipingAnItem,
scrollBehavior: .custom().offset(20).and(.velocity(100))
) {
Text("Swipe me!")
} actions: {
SwipyAction { model in
Button {
print("Delete")
model.unswipe()
} label: {
Image(systemName: "trash")
.foregroundStyle(.white)
}
}
}
Here's a basic example of how to use Swipy:
import SwiftUI
struct MyView: View {
@State private var isSwipingAnItem = false
@State private var items = ["Item 1", "Item 2", "Item 3"]
var body: some View {
ScrollView {
LazyVStack(spacing: 20) {
ForEach(items, id: \.self) { item in
Swipy(isSwipingAnItem: $isSwipingAnItem) { model in
Text(item)
.frame(maxWidth: .infinity)
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.white)
.shadow(color: .black.opacity(0.1), radius: 5, x: 1, y: 2)
)
.padding(.horizontal)
.foregroundColor(.black)
} actions: {
HStack {
SwipyAction { model in
Button {
withAnimation(.bouncy) {
items.removeAll { $0 == item }
model.unswipe()
}
} label: {
Image(systemName: "trash")
.font(.system(size: 20))
.padding(.horizontal)
}
.frame(maxHeight: .infinity)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(16)
}
SwipyAction { model in
Button {
withAnimation(.bouncy) {
model.unswipe()
}
} label: {
Image(systemName: "pencil")
.font(.system(size: 20))
.padding(.horizontal)
}
.frame(maxHeight: .infinity)
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(16)
}
}
}
}
}
.padding(.vertical)
}
.scrollDisabled(isSwipingAnItem)
}
}
You can support the development by making a donation. You have the following options to donate:
You can also support me by buying my MacsyZones app. 🥳
Also see MacsyZones GitHub repo because it is open source too. 🥳
Currency | Address |
---|---|
BTC | bc1qhvlc762kwuzeawedl9a8z0duhs8449nwwc35e2 |
ETH / USDT / USDC | 0x1D99B2a2D85C34d478dD8519792e82B18f861974 |
XMR | 88qvS4sfUnLZ7nehFrz3PG1pWovvEgprcUhkmVLaiL8PVAFgfHjspjKPLhWLj3DUcm92rwNQENbJ1ZbvESdukWvh3epBUty |
Preferably, donating USDT or USDC is recommended but you can donate any of the above currencies. 🥳
We welcome contributions to Swipy. Please see the CONTRIBUTING.md file for more information.
We have adopted a Code of Conduct that we expect project participants to adhere to. Please read CODE_OF_CONDUCT.md so that you can understand what actions will and will not be tolerated.
Copyright (C) 2024, Oğuzhan Eroğlu rohanrhu2@gmail.com (https://meowingcat.io/)
Licensed under MIT License.
See LICENSE for more information.