Skip to content

Instantly share code, notes, and snippets.

@shaps80
Last active October 16, 2019 23:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shaps80/a7092ee9c240604542b12f4a6d51b4ad to your computer and use it in GitHub Desktop.
Save shaps80/a7092ee9c240604542b12f4a6d51b4ad to your computer and use it in GitHub Desktop.
Playback Gesture
import UIKit
/// A gesture optimised for use in video playback. Both delta-offset and target-delta based translations are supported
public final class PlaybackGestureRecognizer: LongPressGestureRecognizer {
/// Specifies the direction of the scrub
public enum Direction {
/// The user is scrubbed forward through time
case forward
/// The user is scrubbed backward through time
case backward
}
/// A type for representing a delta offset value
public enum Delta: Comparable {
case frameByFrame
case slowest
case slower
case slow
case medium
case fast
case faster
case fastest
case custom(framesPerSecond: CGFloat)
/// Returns the number of frames per second this delta represents.
public var framesPerSecond: CGFloat {
switch self {
case .frameByFrame: return 1
case .slowest: return 2
case .slower: return 5
case .slow: return 10
case .medium: return 20
case .fast: return 30
case .faster: return 60
case .fastest: return 120
case let .custom(framesPerSecond): return framesPerSecond
}
}
/// Represents the default set of delta's to be provided to the initializer
public static var defaults: [Delta] {
return [.frameByFrame, .slowest, .slower, .slow, .medium, .fast, .faster, .fastest]
}
public static func < (lhs: Delta, rhs: Delta) -> Bool {
return lhs.framesPerSecond < rhs.framesPerSecond
}
}
// The possible delta values to use for triggering a call to the closure
private let deltas: [Delta]
// The observer to notify about translation delta updates
private let directionObserver: ((Direction) -> Void)?
// The observer to nofify about delta updates
private let deltasObserver: ((Delta, Direction) -> Void)?
/// Initializes the gesture with an `observer` that will be notified for every `delta` change that occurs.
///
/// E.g. if `delta=10`, the `observer` will be called for every 10pt change in `translation`.
///
/// - Parameter delta: The delta that will trigger an update
/// - Parameter observer: The observer to be notified
public init(delta: Delta = .slow, observer: @escaping (Direction) -> Void) {
self.deltas = [delta]
self.directionObserver = observer
self.deltasObserver = nil
super.init(target: nil, action: nil)
addTarget(self, action: #selector(handle(_:)))
}
/// Initializes the gesture with an `observer` that will be notified whenever a `targetDelta` has been reached.
///
/// E.g. if `deltas==[10, 20, 50]` , the `observer` will be notified when the `translation` reaches each of those targets.
///
/// - Parameter deltas: The target delta's that will trigger an update
/// - Parameter observer: The observer that will be notified
public init(targetDeltas deltas: [Delta] = Delta.defaults, observer: @escaping (Delta, Direction) -> Void) {
// to sanitize the deltas, we remove negative values (including 0), sort them in ascending order and then reverse them.
self.deltas = deltas.filter { $0.framesPerSecond > 0 }.sorted().reversed()
self.deltasObserver = observer
self.directionObserver = nil
super.init(target: nil, action: nil)
addTarget(self, action: #selector(handle(_:)))
}
@objc private func handle(_ gesture: PlaybackGestureRecognizer) {
let translation = self.translation(in: view).x
switch gesture.state {
case .began: break
case .changed:
if deltas.count == 1 {
guard !(0...deltas[0].framesPerSecond).contains(translation) else { return }
directionObserver?(translation > 0 ? .forward : .backward)
setTranslation(.zero, in: gesture.view)
} else {
guard let delta = deltas.first(where: { abs(translation) > $0.framesPerSecond }) else { return }
deltasObserver?(delta, translation > 0 ? .forward : .backward)
}
default: break
}
}
}
/// A UILongPressGestureRecognizer with built-in support for translation
open class LongPressGestureRecognizer: UILongPressGestureRecognizer {
// The location of the initial touch
private var initialLocation: CGPoint = .zero
// The current translation
private var translation: CGPoint = .zero
/// The translation of the pan gesture in the coordinate system of the specified view.
/// - Parameter view: The view in whose coordinate system the translation of the pan gesture should be computed. If you want to adjust a view's location to keep it under the user's finger, request the translation in that view's superview's coordinate system.
///
/// The x and y values report the total translation over time. They are not delta values from the last time that the translation was reported. Apply the translation value to the state of the view when the gesture is first recognized—do not concatenate the value each time the handler is called.
open func translation(in view: UIView?) -> CGPoint {
return self.view?.convert(translation, to: view) ?? translation
}
/// Sets the translation value in the coordinate system of the specified view.
/// - Parameter translation: A point that identifies the new translation value.
/// - Parameter view: A view in whose coordinate system the translation is to occur.
open func setTranslation(_ translation: CGPoint, in view: UIView?) {
initialLocation.x += self.translation.x
initialLocation.y += self.translation.y
self.translation = self.view?.convert(translation, to: view) ?? translation
}
}
extension LongPressGestureRecognizer {
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
initialLocation = touches.first?.location(in: view) ?? .zero
translation = .zero
super.touchesBegan(touches, with: event)
}
open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
let location = touches.first?.location(in: view) ?? .zero
translation = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
super.touchesMoved(touches, with: event)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment