import Foundation import UIKit import SwiftSignalKit import AsyncDisplayKit import Display private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool { if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating { return true } for subview in view.subviews { let subviewPoint = view.convert(point, to: subview) if traceDeceleratingScrollView(subview, at: subviewPoint) { return true } } return false } public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer { private let contentAtPoint: (CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? private let present: (PeekControllerContent, UIView, CGRect) -> ViewController? private let updateContent: (PeekControllerContent?) -> Void private let activateBySingleTap: Bool public var longPressEnabled = true public var checkSingleTapActivationAtPoint: ((CGPoint) -> Bool)? private var tapLocation: CGPoint? private var longTapTimer: SwiftSignalKit.Timer? private var pressTimer: SwiftSignalKit.Timer? private let candidateContentDisposable = MetaDisposable() private var candidateContent: (UIView, CGRect, PeekControllerContent)? { didSet { self.updateContent(self.candidateContent?.2) } } private var menuActivation: PeerControllerMenuActivation? private weak var presentedController: PeekController? public init(contentAtPoint: @escaping (CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, UIView, CGRect) -> ViewController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) { self.contentAtPoint = contentAtPoint self.present = present self.updateContent = updateContent self.activateBySingleTap = activateBySingleTap super.init(target: nil, action: nil) } deinit { self.longTapTimer?.invalidate() self.pressTimer?.invalidate() self.candidateContentDisposable.dispose() } private func startLongTapTimer() { guard self.longPressEnabled else { return } self.longTapTimer?.invalidate() let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in self?.longTapTimerFired() }, queue: Queue.mainQueue()) self.longTapTimer = longTapTimer longTapTimer.start() } private func startPressTimer() { self.pressTimer?.invalidate() let pressTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in self?.pressTimerFired() }, queue: Queue.mainQueue()) self.pressTimer = pressTimer pressTimer.start() } private func stopLongTapTimer() { self.longTapTimer?.invalidate() self.longTapTimer = nil } private func stopPressTimer() { self.pressTimer?.invalidate() self.pressTimer = nil } override public func reset() { super.reset() self.stopLongTapTimer() self.stopPressTimer() self.tapLocation = nil self.candidateContent = nil self.menuActivation = nil self.presentedController = nil } private func longTapTimerFired() { guard let tapLocation = self.tapLocation else { return } self.checkCandidateContent(at: tapLocation) } private func pressTimerFired() { if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation { if let presentedController = self.presentedController { if presentedController.isNodeLoaded { (presentedController.displayNode as? PeekControllerNode)?.activateMenu() } self.menuActivation = nil // self.presentedController = nil // self.state = .ended } } } override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) if let view = self.view, let tapLocation = touches.first?.location(in: view) { if traceDeceleratingScrollView(view, at: tapLocation) { self.candidateContent = nil self.state = .failed } else { self.tapLocation = tapLocation self.startLongTapTimer() } } } override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) var activateBySingleTap = self.activateBySingleTap if !activateBySingleTap, let checkSingleTapActivationAtPoint = self.checkSingleTapActivationAtPoint, let tapLocation = self.tapLocation { activateBySingleTap = checkSingleTapActivationAtPoint(tapLocation) } if activateBySingleTap, self.presentedController == nil { self.longTapTimer?.invalidate() self.pressTimer?.invalidate() if let tapLocation = self.tapLocation { self.checkCandidateContent(at: tapLocation, forceActivate: true) } self.state = .ended } else { if let presentedController = self.presentedController, presentedController.isNodeLoaded, let location = touches.first?.location(in: presentedController.view) { (presentedController.displayNode as? PeekControllerNode)?.endDragging(location) self.presentedController = nil self.menuActivation = nil } self.tapLocation = nil self.candidateContent = nil self.longTapTimer?.invalidate() self.pressTimer?.invalidate() self.candidateContentDisposable.set(nil) self.state = .failed } } override public func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) self.tapLocation = nil self.candidateContent = nil self.state = .failed if let presentedController = self.presentedController { self.menuActivation = nil self.presentedController = nil presentedController.dismiss() } } override public func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) if let touch = touches.first, let initialTapLocation = self.tapLocation { let touchLocation = touch.location(in: self.view) if let presentedController = self.presentedController, self.menuActivation == nil { if presentedController.isNodeLoaded { let touchLocation = touch.location(in: presentedController.view) (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(touchLocation) } } else if let menuActivation = self.menuActivation, let presentedController = self.presentedController { switch menuActivation { case .drag: var offset = touchLocation.y - initialTapLocation.y let delta = abs(offset) let factor: CGFloat = 60.0 offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0) if presentedController.isNodeLoaded { // (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset) } case .press: if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if touch.force >= 2.5 { if presentedController.isNodeLoaded { (presentedController.displayNode as? PeekControllerNode)?.activateMenu() self.menuActivation = nil self.presentedController = nil self.candidateContent = nil self.state = .ended self.candidateContentDisposable.set(nil) return } } } if self.pressTimer != nil { let dX = touchLocation.x - initialTapLocation.x let dY = touchLocation.y - initialTapLocation.y if dX * dX + dY * dY > 3.0 * 3.0 { self.startPressTimer() } } if self.presentedController != nil { self.checkCandidateContent(at: touchLocation) } } } else { let dX = touchLocation.x - initialTapLocation.x let dY = touchLocation.y - initialTapLocation.y if dX * dX + dY * dY > 3.0 * 3.0 { self.stopLongTapTimer() self.tapLocation = nil self.candidateContent = nil self.state = .failed } } } } private func checkCandidateContent(at touchLocation: CGPoint, forceActivate: Bool = false) { //print("check begin") if let contentSignal = self.contentAtPoint(touchLocation) { self.candidateContentDisposable.set((contentSignal |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { let processResult: Bool if forceActivate { processResult = true } else { switch strongSelf.state { case .possible, .changed: processResult = true default: processResult = false } } //print("check received, will process: \(processResult), force: \(forceActivate), state: \(strongSelf.state)") if processResult { if let (sourceView, sourceRect, content) = result { if let currentContent = strongSelf.candidateContent { if !currentContent.2.isEqual(to: content) { strongSelf.tapLocation = touchLocation strongSelf.candidateContent = (sourceView, sourceRect, content) strongSelf.menuActivation = content.menuActivation() if let presentedController = strongSelf.presentedController, presentedController.isNodeLoaded { presentedController.sourceView = { return (sourceView, sourceRect) } (presentedController.displayNode as? PeekControllerNode)?.updateContent(content: content) } } } else { if let presentedController = strongSelf.present(content, sourceView, sourceRect) { if let presentedController = presentedController as? PeekController { if forceActivate { strongSelf.candidateContent = nil if case .press = content.menuActivation() { (presentedController.displayNode as? PeekControllerNode)?.activateMenu() } } else { strongSelf.candidateContent = (sourceView, sourceRect, content) strongSelf.menuActivation = content.menuActivation() strongSelf.presentedController = presentedController strongSelf.state = .began switch content.menuActivation() { case .drag: break case .press: if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { if presentedController.traitCollection.forceTouchCapability != .available { strongSelf.startPressTimer() } } else { strongSelf.startPressTimer() } } } } else { if strongSelf.state != .ended { strongSelf.state = .ended } } } } } else if strongSelf.presentedController == nil { if strongSelf.state != .possible && strongSelf.state != .ended { strongSelf.state = .failed } } } } })) } else if self.presentedController == nil { self.state = .failed } } }