rohanrhu/swipy
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. 🥳
Why?
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. 🥳
Features
- Swipe to reveal actions
- Customizable
- Clean and simple API
- Easy integration with existing SwiftUI views
- UX friendly
- Can be used in a
ScrollView
iOS Support
Swipy supports minimum iOS 15.
[!CAUTION] Swipy's
isSwipingAnItembinding is useful withScrollView's.scrollDisabled(_ disabled: Bool)modifier but this modifier requires minimum iOS 16. However, you still haveisSwipingAnItemso, you can avoid scrolling if somehow it istrue.
Installation
Swift Package Manager
You can install Swipy via Swift Package Manager.
Add Package Dependency on Xcode
- In Xcode, File > Add Package Dependencies.
- Enter Swipy GitHub repo address
https://github.com/rohanrhu/Swipyinto search bar. - Add Swipy to your project. 🥳
Add Package Dependency on Package.swift
Add Swipy as a dependency in your Package.swift file:
dependencies: [
// ...
.package(url: "https://github.com/rohanrhu/Swipy", from: "1.0.0")
]Or, Swipy is a single Swipy.swift
To use Swipy in your project, simply copy the Swipy.swift file into your project.
Usage
Swipy is easy. It has two main views: Swipy and SwipyAction.
Make Something Swipeable
In most cases, you'll want to make something swipeable in a ScrollView. First, you need to define a isSwipingAnItem state.
Defining isSwipingAnItem
We always need to have a isSwipingAnItem state and bind it to all Swipy sub-views.
[!IMPORTANT] If your
Swipyviews are inside aScrollView(or any kind of scrollables), yourSwipyviews will set yourisSwipingAnItemstate by bindings to make you able to disable/enable container's scroll properly.
@State private var isSwipingAnItem = falseand you'll use this for your ScrollView's .scrollDisabled(_ disabled: Bool) modifier.
Making a View Swipeable
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!")
}Adding Actions
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
SwipyModel is a model that is passed to the SwipyAction's content closure. It provides information about the current swipe state.
Properties
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.
Methods
swipe(): Swipes the item.unswipe(): Unswipes the item.
[!NOTE] You can use
SwipyModelto unswipe your item after an action is performed.
Unswiping when action is performed imperatively
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)
}
}Swiping an item imperatively
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)
}
}
}Customizations
Swipy has customizations to make it fit your app's design and provide best user experience.
View: Swipy
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, aSwipyHorizontalMarginstruct. - (Optional)
swipeThreshold: The swipe threshold calculator function. - (Optional)
swipeBehavior: The behavior for swipe actions, aSwipySwipeBehaviorstruct. - (Optional)
scrollBehavior: The behavior for scroll actions, aSwipyScrollBehaviorstruct.
Option: SwipyActionsMargin: SwipyHorizontalMargin(leading: Double, trailing: Double)
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)
Option: swipeThreshold: (SwipyModel) -> Double
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
Option: swipeBehavior: SwipySwipeBehavior
The behavior for swipe actions. You can use predefined behaviors or create custom ones.
Option: scrollBehavior: SwipyScrollBehavior
The behavior for scroll actions. You can use predefined behaviors or create custom ones.
View: SwipyAction
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.
Behaviors
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.
Swipe Behavior
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
falseto 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 / ... / }.
Predefined Swipe Behaviors
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)
}
}
}Combining Swipe Behaviors
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)
}
}
}Custom Swipe Behavior
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)
}
}
}Scroll Behavior
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
truefor starting to prevent swiping for the current ongoing user drag session.
Predefined Scroll Behaviors
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)
}
}
}Custom Scroll Behavior
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)
}
}
}Combining Scroll Behaviors
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)
}
}
}Examples
Basic Example
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)
}
}Donations ❤️
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. 🥳
Cryptocurrency Donations
| 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. 🥳
Contributing
We welcome contributions to Swipy. Please see the CONTRIBUTING.md file for more information.
Code of Conduct
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.
License
Copyright (C) 2024, Oğuzhan Eroğlu <rohanrhu2@gmail.com> (<https://meowingcat.io/>)
Licensed under MIT License.
See LICENSE for more information.
Package Metadata
Repository: rohanrhu/swipy
Default branch: main
README: README.md