|
| 1 | +import BlueprintUI |
| 2 | +import BlueprintUICommonControls |
| 3 | +import Foundation |
| 4 | +import UIKit |
| 5 | + |
| 6 | +public struct LongPress: Element { |
| 7 | + |
| 8 | + public var wrappedElement: Element |
| 9 | + public var onLongPress: () -> Void |
| 10 | + |
| 11 | + public init(onLongPress: @escaping () -> Void, wrapping element: Element) { |
| 12 | + wrappedElement = element |
| 13 | + self.onLongPress = onLongPress |
| 14 | + } |
| 15 | + |
| 16 | + public var content: ElementContent { |
| 17 | + ElementContent(child: wrappedElement) |
| 18 | + } |
| 19 | + |
| 20 | + public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? { |
| 21 | + LongPressableView.describe { config in |
| 22 | + config[\.onLongPress] = onLongPress |
| 23 | + } |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +extension Element { |
| 28 | + |
| 29 | + /// Wraps the element and calls the provided closure when tapped. |
| 30 | + func onLongPress(_ callback: @escaping () -> Void) -> LongPress { |
| 31 | + LongPress(onLongPress: callback, wrapping: self) |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +// MARK: LongPressableView |
| 36 | + |
| 37 | +private final class LongPressableView: UIView, UIGestureRecognizerDelegate { |
| 38 | + |
| 39 | + var onLongPress: (() -> Void)? = nil |
| 40 | + let longPressRecognizer: UILongPressGestureRecognizer |
| 41 | + private static let defaultPressDuration: TimeInterval = 0.5 |
| 42 | + private static let adjustedPressDuration: TimeInterval = 3.0 |
| 43 | + |
| 44 | + override init(frame: CGRect) { |
| 45 | + let longPressRecognizer = UILongPressGestureRecognizer() |
| 46 | + self.longPressRecognizer = longPressRecognizer |
| 47 | + |
| 48 | + super.init(frame: frame) |
| 49 | + |
| 50 | + longPressRecognizer.addTarget(self, action: #selector(longPressed(_:))) |
| 51 | + longPressRecognizer.delegate = self |
| 52 | + addGestureRecognizer(longPressRecognizer) |
| 53 | + |
| 54 | + updateView() |
| 55 | + } |
| 56 | + |
| 57 | + func updateView() { |
| 58 | + longPressRecognizer.minimumPressDuration = UILargeContentViewerInteraction.isEnabled ? Self.adjustedPressDuration : Self.defaultPressDuration |
| 59 | + } |
| 60 | + |
| 61 | + func gestureRecognizer( |
| 62 | + _ gestureRecognizer: UIGestureRecognizer, |
| 63 | + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer |
| 64 | + ) -> Bool { |
| 65 | + (gestureRecognizer == longPressRecognizer) && (otherGestureRecognizer == ancestorLargeContentViewerInteraction?.gestureRecognizerForExclusionRelationship) |
| 66 | + } |
| 67 | + |
| 68 | + var ancestorLargeContentViewerInteraction: UILargeContentViewerInteraction? { |
| 69 | + sequence(first: self, next: { $0.superview }) |
| 70 | + .dropFirst() |
| 71 | + .lazy |
| 72 | + .compactMap { $0 as? Accessibility.LargeContentViewerInteractionContainerViewable } |
| 73 | + .first? |
| 74 | + .largeContentViewerInteraction |
| 75 | + } |
| 76 | + |
| 77 | + required init?(coder aDecoder: NSCoder) { |
| 78 | + fatalError("init(coder:) has not been implemented") |
| 79 | + } |
| 80 | + |
| 81 | + @objc private func longPressed(_ sender: UILongPressGestureRecognizer) { |
| 82 | + // This function is called multiple times during the lifecycle of a single long-press, |
| 83 | + // so we only listen for the "begin" state to avoid calling the onLongPress callback too many times |
| 84 | + guard sender.state == .began else { return } |
| 85 | + |
| 86 | + onLongPress?() |
| 87 | + } |
| 88 | +} |
| 89 | + |
0 commit comments