1 // Copyright (c) 2018, Applidium. All rights reserved 2 // ScrollViewOverlayTranslationDriver.swift 3 // OverlayContainer 4 // 5 // Created by Gaétan Zanella on 29/11/2018. 6 // 7 8 import UIKit 9 10 class ScrollViewOverlayTranslationDriver: OverlayTranslationDriver, OverlayScrollViewDelegate { 11 12 weak var translationController: OverlayTranslationController? 13 weak var scrollView: UIScrollView? 14 15 private let scrollViewDelegateProxy = OverlayScrollViewDelegateProxy() 16 17 // (gz) 2018-11-27 The overlay transaction is not always equal to the scroll view translation. 18 // The user can scroll bottom then drag the overlay up repeatedly in a single gesture. 19 private var overlayTranslation: CGFloat = 0 20 private var scrollViewTranslation: CGFloat = 0 21 private var lastContentOffsetWhileScrolling: CGPoint = .zero 22 23 // MARK: - Life Cycle 24 25 init(translationController: OverlayTranslationController, scrollView: UIScrollView) { 26 self.translationController = translationController 27 self.scrollView = scrollView 28 scrollViewDelegateProxy.forward(to: self, delegateInvocationsFrom: scrollView) 29 lastContentOffsetWhileScrolling = scrollView.contentOffset 30 } 31 32 // MARK: - OverlayTranslationDriver 33 cleannull34 func clean() { 35 scrollViewDelegateProxy.cancelForwarding() 36 } 37 38 // MARK: - OverlayScrollViewDelegate 39 overlayScrollViewWillBeginDraggingnull40 func overlayScrollViewWillBeginDragging(_ scrollView: UIScrollView) { 41 translationController?.startOverlayTranslation() 42 } 43 overlayScrollViewDidScrollnull44 func overlayScrollViewDidScroll(_ scrollView: UIScrollView) { 45 guard let controller = translationController else { return } 46 let previousTranslation = scrollViewTranslation 47 scrollViewTranslation = scrollView.panGestureRecognizer.translation(in: scrollView).y 48 if shouldDragOverlay(following: scrollView) { 49 if scrollView.isContentOriginInBounds { 50 overlayTranslation += -scrollView.topOffsetInContent 51 } else { 52 overlayTranslation += scrollViewTranslation - previousTranslation 53 } 54 let offset = adjustedContentOffset(dragging: scrollView) 55 lastContentOffsetWhileScrolling = offset 56 scrollView.contentOffset = offset // Warning : calls `overlayScrollViewDidScroll(_:)` again 57 controller.dragOverlay(withOffset: overlayTranslation, usesFunction: false) 58 } else { 59 lastContentOffsetWhileScrolling = scrollView.contentOffset 60 } 61 } 62 63 func overlayScrollView(_ scrollView: UIScrollView, 64 willEndDraggingwithVelocity velocity: CGPoint, 65 targetContentOffset: UnsafeMutablePointer<CGPoint>) { 66 guard let controller = translationController else { return } 67 overlayTranslation = 0 68 scrollViewTranslation = 0 69 // (gz) 2018-11-27 We reset the translation each time the user ends dragging. 70 // Otherwise the calculation is wrong in `overlayScrollViewDidScroll(_:)` 71 // if the user drags the overlay while the animation did not finish. 72 scrollView.panGestureRecognizer.setTranslation(.zero, in: nil) 73 // (gz) 2018-01-24 We adjust the content offset and the velocity only if the overlay will be dragged. 74 switch controller.translationPosition { 75 case .bottom where targetContentOffset.pointee.y > -scrollView.oc_adjustedContentInset.top: 76 // (gz) 2018-11-26 The user raises its finger in the bottom position 77 // and the content offset will exceed the top content inset. 78 targetContentOffset.pointee.y = -scrollView.oc_adjustedContentInset.top 79 case .inFlight where !controller.overlayHasReachedANotch(): 80 targetContentOffset.pointee.y = lastContentOffsetWhileScrolling.y 81 case .top, .bottom, .inFlight, .stationary: 82 break 83 } 84 // If the overlay is in flight and the user scrolls bottom, we ignore the velocity and we do not 85 // modify the target offset. 86 let adjustedVelocity: CGPoint 87 if shouldDragOverlay(following: scrollView) { 88 adjustedVelocity = velocity 89 } else { 90 adjustedVelocity = .zero 91 } 92 controller.endOverlayTranslation(withVelocity: adjustedVelocity) 93 } 94 95 // MARK: - Private 96 shouldDragOverlaynull97 private func shouldDragOverlay(following scrollView: UIScrollView) -> Bool { 98 guard let controller = translationController, scrollView.isTracking else { return false } 99 let velocity = scrollView.panGestureRecognizer.velocity(in: nil).y 100 let movesUp = velocity < 0 101 switch controller.translationPosition { 102 case .bottom: 103 return !scrollView.isContentOriginInBounds && scrollView.scrollsUp 104 case .top: 105 return scrollView.isContentOriginInBounds && !movesUp 106 case .inFlight: 107 return scrollView.isContentOriginInBounds || scrollView.scrollsUp 108 case .stationary: 109 return false 110 } 111 } 112 adjustedContentOffsetnull113 private func adjustedContentOffset(dragging scrollView: UIScrollView) -> CGPoint { 114 guard let controller = translationController else { return .zero } 115 var contentOffset = lastContentOffsetWhileScrolling 116 let topInset = -scrollView.oc_adjustedContentInset.top 117 switch controller.translationPosition { 118 case .inFlight, .top: 119 // (gz) 2018-11-26 The user raised its finger in the top or in flight positions while scrolling bottom. 120 // If the scroll animation did not finish when the user translates the overlay, 121 // the content offset may have exceeded the top inset. We adjust it. 122 if contentOffset.y < topInset { 123 contentOffset.y = topInset 124 } 125 case .bottom, .stationary: 126 break 127 } 128 // (gz) 2018-11-26 Between two `overlayScrollViewDidScroll:` calls, 129 // the scrollView exceeds the top contentInset. We adjust the target. 130 if (contentOffset.y - topInset) * (scrollView.contentOffset.y - topInset) < 0 { 131 contentOffset.y = topInset 132 } 133 return contentOffset 134 } 135 } 136