Issue #475

For a snack bar or image viewing, it’s handy to be able to just flick or toss to dismiss

We can use UIKit Dynamic, which was introduced in iOS 7, to make this happen.

Use UIPanGestureRecognizer to drag view around, UISnapBehavior to make view snap back to center if velocity is low, and UIPushBehavior to throw view in the direction of the gesture.

import UIKit

final class FlickHandler {
    private let viewToMove: UIView
    private let referenceView: UIView
    private var panGR: UIPanGestureRecognizer!
    private let animator: UIDynamicAnimator

    private var snapBehavior: UISnapBehavior?
    private var pushBehavior: UIPushBehavior?
    private let debouncer = Debouncer(delay: 0.5)

    var onFlick: () -> Void = {}

    init(viewToMove: UIView, referenceView: UIView) {
        self.viewToMove = viewToMove
        self.referenceView = referenceView
        self.animator = UIDynamicAnimator(referenceView: referenceView)
        self.panGR = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        viewToMove.addGestureRecognizer(panGR)
    }

    @objc private func handleGesture(_ gr: UIPanGestureRecognizer) {
        switch gr.state {
        case .began:
            handleBegin()
        case .changed:
            handleChange(gr)
        default:
            handleEnd(gr)
        }
    }

    private func handleBegin() {
        animator.removeAllBehaviors()
    }

    private func handleChange(_ gr: UIPanGestureRecognizer) {
        let translation = panGR.translation(in: referenceView)
        viewToMove.transform = CGAffineTransform(
            translationX: translation.x,
            y: translation.y
        )
    }

    private func handleEnd(_ gr: UIPanGestureRecognizer) {
        let velocity = gr.velocity(in: gr.view)
        let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
        if magnitude > 1000 {
            animator.removeAllBehaviors()

            let pushBehavior = UIPushBehavior(items: [viewToMove], mode: .instantaneous)
            pushBehavior.pushDirection = CGVector(dx: velocity.x, dy: velocity.y)
            pushBehavior.magnitude = magnitude / 35

            self.pushBehavior = pushBehavior
            animator.addBehavior(pushBehavior)

            onFlick()
            debouncer.run { [weak self] in
                self?.animator.removeAllBehaviors()
            }
        } else {
            let snapBehavior = UISnapBehavior(
                item: viewToMove,
                snapTo: viewToMove.center
            )

            self.snapBehavior = snapBehavior
            animator.addBehavior(snapBehavior)
        }
    }
}