Created
November 2, 2020 03:22
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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