Skip to content

Instantly share code, notes, and snippets.

@kieranb662
Created November 2, 2020 03:22
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kieranb662/72aad7780e435f4620c357dea18135e8 to your computer and use it in GitHub Desktop.
Save kieranb662/72aad7780e435f4620c357dea18135e8 to your computer and use it in GitHub Desktop.
Sticky drag modifier. This modifier adds a drag gesture to the view it modifies. This drag gesture behaves a lot like a rubber band be pulled to its breaking point.
// Swift toolchain version 5.0
// Running macOS version 10.15
// Created on 11/1/20.
//
// Author: Kieran Brown
//
import SwiftUI
import UIKit
// MARK: - CGSize: VectorArithmetic
extension CGSize: VectorArithmetic {
public mutating func scale(by rhs: Double) {
self.width = CGFloat(rhs)*self.width
self.height = CGFloat(rhs)*self.height
}
public var magnitudeSquared: Double {
Double(width*width+height*height)
}
}
// MARK: - CGSize: AdditiveArithmetic
extension CGSize: AdditiveArithmetic {
public static func - (lhs: CGSize, rhs: CGSize) -> CGSize {
CGSize(width: lhs.width-rhs.width,
height: lhs.height-rhs.height)
}
public static func + (lhs: CGSize, rhs: CGSize) -> CGSize {
return CGSize(width: lhs.width+rhs.width,
height: lhs.height+rhs.height)
}
}
// MARK: - Sticky Drag Modifier
struct StickyDragModifier: ViewModifier {
@State private var offset: CGSize = .zero
@State private var dragState: CGSize = .zero
@State private var snapped: Bool = false
/// The threshold distance a user must drag before the stickiness effect is removed.
/// This has a snap like animated effect.
private var snapDistance: Double
/// A value constrained between 0(least sticky) and 1(most sticky).
private var stickiness: Double
/// Sticky drag modifier. This modifier adds a drag gesture to the view it modifies.
/// This drag gesture behaves a lot like a rubber band be pulled to its breaking point.
/// Upon the user dragging further than the `snapDistance` a haptic impact is played
/// and the view can be dragged normally until the gesture ends.
/// - Parameters:
/// - stickiness: A value constrained between 0(least sticky) and 1(most sticky).
/// - snapDistance: The threshold distance a user must drag before the stickiness effect is removed. This has a snap like animated effect.
public init(stickiness: Double = 0.33, snapDistance: Double = 150) {
self.snapDistance = max(1,snapDistance)
self.stickiness = max(min(stickiness,1), 0)
}
private func stickify(_ translation: CGSize) -> CGSize {
let magnitude = sqrt(translation.magnitudeSquared)
if magnitude > snapDistance || snapped {
if !snapped {
Self.mediumImpact()
snapped = true
}
return translation
}
var copy = translation
let scale = 1-log(1 + magnitude/(snapDistance*(2-stickiness))) // dont worry about this math, if it fits it ships
copy.scale(by: scale)
return copy
}
private static func mediumImpact() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
}
func body(content: Content) -> some View {
content
.offset(offset+dragState)
.gesture(
DragGesture()
.onChanged({ self.dragState = stickify($0.translation) })
.onEnded({
self.offset += $0.translation
self.dragState = .zero
self.snapped = false
})
)
.animation(.linear)
}
}
// MARK: - View Extension
extension View {
func stickyDraggable() -> some View {
self.modifier(StickyDragModifier())
}
}
// MARK: - Example
struct StickyDragExample: View {
var body: some View {
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
.stickyDraggable()
}
}
struct StickyDragExample_Previews: PreviewProvider {
static var previews: some View {
StickyDragExample()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment