mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
118 lines
3.8 KiB
Swift
118 lines
3.8 KiB
Swift
//
|
|
// OverscrollGestureHandler.swift
|
|
// Yattee
|
|
//
|
|
// UIKit gesture handler for detecting overscroll pull-down gestures on UIScrollView.
|
|
// When user pulls down at scroll top, disables bounce and forwards drag events for smooth
|
|
// panel collapse animation.
|
|
//
|
|
|
|
#if os(iOS)
|
|
import UIKit
|
|
|
|
/// Coordinates overscroll detection on a UIScrollView, calling back during pull-down gestures
|
|
/// when the scroll is at top. Disables bounce during the gesture to allow smooth animation.
|
|
final class OverscrollGestureHandler: NSObject, UIGestureRecognizerDelegate {
|
|
// MARK: - Properties
|
|
|
|
weak var scrollView: UIScrollView?
|
|
var onDragChanged: ((CGFloat) -> Void)?
|
|
var onDragEnded: ((CGFloat, CGFloat) -> Void)?
|
|
|
|
/// Whether we're currently tracking an overscroll gesture
|
|
private var isTracking = false
|
|
|
|
/// The pan gesture recognizer we add to the scroll view
|
|
private var panRecognizer: UIPanGestureRecognizer?
|
|
|
|
// MARK: - Setup
|
|
|
|
/// Attaches the pan gesture recognizer to the scroll view.
|
|
func attach(to scrollView: UIScrollView) {
|
|
detach()
|
|
|
|
self.scrollView = scrollView
|
|
|
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
pan.delegate = self
|
|
scrollView.addGestureRecognizer(pan)
|
|
panRecognizer = pan
|
|
}
|
|
|
|
/// Removes the pan gesture recognizer from the scroll view.
|
|
func detach() {
|
|
if let recognizer = panRecognizer, let view = recognizer.view {
|
|
view.removeGestureRecognizer(recognizer)
|
|
}
|
|
panRecognizer = nil
|
|
scrollView = nil
|
|
isTracking = false
|
|
}
|
|
|
|
// MARK: - Gesture Handling
|
|
|
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
guard let scrollView else { return }
|
|
|
|
switch gesture.state {
|
|
case .began:
|
|
// Start tracking - disable bounce so we can control the movement
|
|
isTracking = true
|
|
scrollView.bounces = false
|
|
|
|
case .changed:
|
|
let translation = gesture.translation(in: gesture.view)
|
|
// Only forward positive (pull down) translations
|
|
if translation.y > 0 {
|
|
onDragChanged?(translation.y)
|
|
}
|
|
|
|
case .ended, .cancelled:
|
|
// Re-enable bounce
|
|
scrollView.bounces = true
|
|
isTracking = false
|
|
|
|
let translation = gesture.translation(in: gesture.view)
|
|
let velocity = gesture.velocity(in: gesture.view)
|
|
// Calculate predicted end position
|
|
let decelerationTime: CGFloat = 0.3
|
|
let predicted = translation.y + velocity.y * decelerationTime
|
|
|
|
onDragEnded?(translation.y, predicted)
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
// MARK: - UIGestureRecognizerDelegate
|
|
|
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
guard let pan = gestureRecognizer as? UIPanGestureRecognizer,
|
|
let scrollView else {
|
|
return false
|
|
}
|
|
|
|
let velocity = pan.velocity(in: pan.view)
|
|
|
|
// Only begin if:
|
|
// 1. Scroll view is at top (contentOffset.y <= 0)
|
|
// 2. User is pulling down (velocity.y > 0)
|
|
// 3. Vertical movement is dominant (to not interfere with horizontal scrolling)
|
|
let isAtTop = scrollView.contentOffset.y <= 0
|
|
let isPullingDown = velocity.y > 0
|
|
let isVerticalDominant = abs(velocity.y) > abs(velocity.x)
|
|
|
|
return isAtTop && isPullingDown && isVerticalDominant
|
|
}
|
|
|
|
func gestureRecognizer(
|
|
_ gestureRecognizer: UIGestureRecognizer,
|
|
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
|
|
) -> Bool {
|
|
// Don't allow simultaneous recognition - we take over when overscrolling
|
|
false
|
|
}
|
|
}
|
|
#endif
|