Skip to content

Instantly share code, notes, and snippets.

@boska
Last active September 17, 2024 07:05
Show Gist options
  • Select an option

  • Save boska/70778fb27c1fa741c05fde57c7e951a0 to your computer and use it in GitHub Desktop.

Select an option

Save boska/70778fb27c1fa741c05fde57c7e951a0 to your computer and use it in GitHub Desktop.
/* Example
ZStack(alignment: .top) {
ForEach(cardModels, id: \.id) { model in
CardView(model)
.swipeable { swipedRight in
// store.swipe(swipedRight)
}
}
}
*/
struct SwipeableCardModifier: ViewModifier {
@GestureState private var offset = CGSize.zero
@State private var finalOffset = CGSize.zero
@State private var actionOffset = CGSize.zero
var swipedRight: ((Bool) -> Void)?
@GestureState private var isDragging = false
func body(content: Content) -> some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
content
.offset(x: finalOffset.width + offset.width, y: 0)
.rotationEffect(.degrees(Double((finalOffset.width + offset.width) / (geometry.size.width * 0.1))))
.gesture(
DragGesture()
.updating($offset) { value, state, _ in
state = value.translation
}
.onEnded { value in
finalOffset.width += value.translation.width
finalOffset.height += value.translation.height
swipeCard(width: value.translation.width, screenWidth: geometry.size.width)
}
)
Image(systemName: offset.width < 0 ? "heart.slash" : "heart")
.font(.system(size: 50))
.foregroundColor(.white)
.scaleEffect(scale(for: geometry.size.width))
.opacity(actionOpacity(for: geometry.size.width))
.offset(actionOffset)
}
}
}
func scale(for screenWidth: CGFloat) -> CGFloat {
let minScale: CGFloat = 1
let maxScale: CGFloat = 1.5
let scale = abs(offset.width) / (screenWidth * 0.25)
return max(min(scale, maxScale), minScale)
}
func actionOpacity(for screenWidth: CGFloat) -> Double {
let threshold: CGFloat = screenWidth * 0.125
let maxOpacity: Double = 1
let minOpacity: Double = 0
let opacity = Double(abs(offset.width) - threshold) / Double(threshold)
return max(min(opacity, maxOpacity), minOpacity)
}
private func swipeCard(width: CGFloat, screenWidth: CGFloat) {
let swipeThreshold = screenWidth * 0.3
let swipeDistance = screenWidth * 1.25
if let swipedRight {
switch width {
case -swipeDistance ... -swipeThreshold:
withAnimation(.easeInOut(duration: 0.2)) {
finalOffset = CGSize(width: -swipeDistance, height: 0)
actionOffset = CGSize(width: 0, height: -100)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
swipedRight(false) // Swiped left
}
case swipeThreshold ... swipeDistance:
withAnimation(.easeInOut(duration: 0.2)) {
finalOffset = CGSize(width: swipeDistance, height: 0)
actionOffset = CGSize(width: 0, height: -100)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
swipedRight(true) // Swiped right
}
default:
withAnimation(.easeInOut(duration: 0.2)) {
finalOffset = .zero
}
}
} else {
withAnimation(.easeInOut(duration: 0.1)) {
finalOffset = .zero
}
}
}
}
extension View {
func swipeable(swipedRight: ((Bool) -> Void)? = nil) -> some View {
self.modifier(SwipeableCardModifier(swipedRight: swipedRight))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment