1 // Copyright (c) 2018, Applidium. All rights reserved 2 // HeightConstraintOverlayTranslationController.swift 3 // OverlayContainer 4 // 5 // Created by Gaétan Zanella on 29/11/2018. 6 // 7 8 import UIKit 9 10 typealias TranslationCompletionBlock = () -> Void 11 12 enum TranslationType { 13 case toIndex(Int), basedOnTargetPolicy, toLastReachedNotchIndex 14 } 15 16 private struct TranslationMetaData { 17 let isAnimated: Bool 18 let velocity: CGPoint 19 let type: TranslationType 20 } 21 22 protocol HeightConstraintOverlayTranslationControllerDelegate: AnyObject { overlayViewControllernull23 func overlayViewController(for translationController: OverlayTranslationController) -> UIViewController? 24 25 func translationController(_ translationController: OverlayTranslationController, 26 willMoveOverlayToNotchAt index: Int) 27 func translationController(_ translationController: OverlayTranslationController, 28 didMoveOverlayToNotchAt index: Int) 29 30 func translationControllerWillStartDraggingOverlay(_ translationController: OverlayTranslationController) 31 func translationController(_ translationController: OverlayTranslationController, 32 willEndDraggingAtVelocity velocity: CGPoint) 33 func translationController(_ translationController: OverlayTranslationController, 34 willTranslateOverlayWith transitionCoordinator: OverlayContainerTransitionCoordinator) 35 36 func translationControllerDidScheduleTranslations(_ translationController: OverlayTranslationController) 37 } 38 39 class HeightConstraintOverlayTranslationController: OverlayTranslationController { 40 41 weak var delegate: HeightConstraintOverlayTranslationControllerDelegate? 42 43 44 private var overlayViewController: UIViewController? { 45 return delegate?.overlayViewController(for: self) 46 } 47 48 private var lastScheduledTranslationAnimator: UIViewImplicitlyAnimating? 49 50 private var translationEndNotchIndex = 0 51 private var deferredTranslation: TranslationMetaData? 52 private var deferredTranslationCompletionBlocks: [TranslationCompletionBlock] = [] 53 54 private var translationStartHeight: CGFloat = 0.0 55 private var translationEndNotchHeight: CGFloat { 56 return configuration.heightForNotch(at: translationEndNotchIndex) 57 } 58 59 private let configuration: OverlayContainerConfiguration 60 private let translationHeightConstraint: NSLayoutConstraint 61 private var isDragging = false 62 63 // MARK: - Life Cycle 64 65 init(translationHeightConstraint: NSLayoutConstraint, 66 configuration: OverlayContainerConfiguration) { 67 self.translationHeightConstraint = translationHeightConstraint 68 self.configuration = configuration 69 } 70 71 // MARK: - Public 72 73 func hasPendingTranslation() -> Bool { 74 return deferredTranslation != nil 75 } 76 77 func performDeferredTranslations() { 78 guard let overlay = delegate?.overlayViewController(for: self), 79 let deferredTranslation = deferredTranslation else { 80 return 81 } 82 let completions = deferredTranslationCompletionBlocks 83 self.deferredTranslation = nil 84 self.deferredTranslationCompletionBlocks = [] 85 let targetIndex: Int 86 switch deferredTranslation.type { 87 case .basedOnTargetPolicy: 88 let context = ConcreteOverlayContainerContextTargetNotchPolicy( 89 isDragging: false, 90 overlayViewController: overlay, 91 overlayTranslationHeight: translationHeight, 92 velocity: deferredTranslation.velocity, 93 notchHeightByIndex: configuration.notchHeightByIndex, 94 reachableIndexes: enabledNotchIndexes() 95 ) 96 let policy = configuration.overlayTargetNotchPolicy(forOverlay: overlay) 97 targetIndex = policy.targetNotchIndex(using: context) 98 case let .toIndex(index): 99 targetIndex = index 100 case .toLastReachedNotchIndex: 101 targetIndex = lastTranslationEndNotchIndex 102 } 103 delegate?.translationController(self, willMoveOverlayToNotchAt: targetIndex) 104 let velocity = deferredTranslation.velocity 105 let isAnimated = deferredTranslation.isAnimated 106 translationEndNotchIndex = targetIndex 107 let targetHeight = configuration.heightForNotch(at: translationEndNotchIndex) 108 if isAnimated { 109 let height = translationHeight 110 let context = ConcreteOverlayContainerContextTransitioning( 111 isDragging: false, 112 isCancelled: false, 113 isAnimated: true, 114 overlayViewController: overlay, 115 overlayTranslationHeight: height, 116 velocity: velocity, 117 targetNotchIndex: translationEndNotchIndex, 118 targetTranslationHeight: targetHeight, 119 notchHeightByIndex: configuration.notchHeightByIndex, 120 reachableIndexes: enabledNotchIndexes() 121 ) 122 let animationController = configuration.animationController(forOverlay: overlay) 123 let animator = animationController.interruptibleAnimator(using: context) 124 let coordinator = InterruptibleAnimatorOverlayContainerTransitionCoordinator( 125 animator: animator, 126 context: context 127 ) 128 animator.addCompletion?({ [weak self] _ in 129 guard let self = self else { return } 130 if self.lastScheduledTranslationAnimator === animator { 131 self.delegate?.translationController(self, didMoveOverlayToNotchAt: targetIndex) 132 self.lastScheduledTranslationAnimator = nil 133 } else { 134 coordinator.markAsCancelled() 135 } 136 completions.forEach { $0() } 137 }) 138 delegate?.translationController(self, willTranslateOverlayWith: coordinator) 139 updateConstraint(toHeight: targetHeight) 140 animator.startAnimation() 141 lastScheduledTranslationAnimator = animator 142 } else { 143 translateOverlayWithoutAnimation(toHeight: targetHeight, isDragging: false) 144 completions.forEach { $0() } 145 delegate?.translationController(self, didMoveOverlayToNotchAt: targetIndex) 146 } 147 } 148 149 func scheduleOverlayTranslation(_ translationType: TranslationType, 150 velocity: CGPoint, 151 animated: Bool, 152 completion: (() -> Void)? = nil) { 153 deferredTranslation = TranslationMetaData( 154 isAnimated: animated, 155 velocity: velocity, 156 type: translationType 157 ) 158 completion.flatMap { deferredTranslationCompletionBlocks.append($0) } 159 } 160 161 // MARK: - OverlayTranslationController 162 163 // Accessors 164 165 var lastTranslationEndNotchIndex: Int { 166 return translationEndNotchIndex 167 } 168 169 var translationHeight: CGFloat { 170 return translationHeightConstraint.constant 171 } 172 173 var translationPosition: OverlayTranslationPosition { 174 let isAtTop = translationHeight == maximumReachableNotchHeight() 175 let isAtBottom = translationHeight == minimumReachableNotchHeight() 176 if isAtTop && isAtBottom { 177 return .stationary 178 } 179 if isAtTop { 180 return .top 181 } else if isAtBottom { 182 return .bottom 183 } else { 184 return .inFlight 185 } 186 } 187 188 func isDraggable(at point: CGPoint, in coordinateSpace: UICoordinateSpace) -> Bool { 189 guard let overlay = overlayViewController else { return false } 190 return configuration.shouldStartDraggingOverlay( 191 overlay, 192 at: point, 193 in: coordinateSpace 194 ) 195 } 196 197 func overlayHasReachedANotch() -> Bool { 198 return enabledNotchIndexes().contains { 199 configuration.heightForNotch(at: $0) == translationHeight 200 } 201 } 202 203 func startOverlayTranslation() { 204 isDragging = false 205 translationStartHeight = translationHeight 206 } 207 208 func dragOverlay(withOffset offset: CGFloat, usesFunction: Bool) { 209 guard let viewController = overlayViewController else { return } 210 let maximumHeight = maximumReachableNotchHeight() 211 let minimumHeight = minimumReachableNotchHeight() 212 let translation = translationStartHeight - offset 213 let height: CGFloat 214 if usesFunction { 215 let parameters = ConcreteOverlayTranslationParameters( 216 minimumHeight: minimumHeight, 217 maximumHeight: maximumHeight, 218 translation: translation 219 ) 220 let function = configuration.overlayTranslationFunction(using: parameters, for: viewController) 221 height = function.overlayTranslationHeight(using: parameters) 222 } else { 223 height = max(minimumHeight, min(maximumHeight, translation)) 224 } 225 if height != translationHeightConstraint.constant, !isDragging { 226 delegate?.translationControllerWillStartDraggingOverlay(self) 227 isDragging = true 228 } 229 translateOverlayWithoutAnimation(toHeight: max(height, 0), isDragging: true) 230 } 231 232 func endOverlayTranslation(withVelocity velocity: CGPoint) { 233 if isDragging { 234 delegate?.translationController(self, willEndDraggingAtVelocity: velocity) 235 } 236 guard overlayHasAmibiguousTranslationHeight() else { return } 237 scheduleOverlayTranslation(.basedOnTargetPolicy, velocity: velocity, animated: true) 238 delegate?.translationControllerDidScheduleTranslations(self) 239 } 240 241 // MARK: - Private 242 243 private func overlayHasAmibiguousTranslationHeight() -> Bool { 244 let heights = enabledNotchIndexes().map { configuration.heightForNotch(at: $0) } 245 guard let index = heights.firstIndex(where: { $0 == translationHeight }) else { 246 return true 247 } 248 return configuration.heightForNotch(at: index) != translationEndNotchHeight 249 } 250 251 private func translateOverlayWithoutAnimation(toHeight height: CGFloat, isDragging: Bool) { 252 guard let overlay = overlayViewController else { return } 253 let context = ConcreteOverlayContainerContextTransitioning( 254 isDragging: isDragging, 255 isCancelled: false, 256 isAnimated: false, 257 overlayViewController: overlay, 258 overlayTranslationHeight: height, 259 velocity: .zero, 260 targetNotchIndex: 0, 261 targetTranslationHeight: height, 262 notchHeightByIndex: configuration.notchHeightByIndex, 263 reachableIndexes: enabledNotchIndexes() 264 ) 265 let coordinator = DraggingOverlayContainerTransitionCoordinator(context: context) 266 updateConstraint(toHeight: height) 267 delegate?.translationController(self, willTranslateOverlayWith: coordinator) 268 coordinator.performCompletions(with: context) 269 } 270 271 private func updateConstraint(toHeight height: CGFloat) { 272 guard translationHeightConstraint.constant != height else { return } 273 translationHeightConstraint.constant = height 274 } 275 276 private func enabledNotchIndexes() -> [Int] { 277 guard let controller = overlayViewController else { return [] } 278 return configuration.enabledNotchIndexes(for: controller) 279 } 280 281 private func minimumReachableNotchHeight() -> CGFloat { 282 let minimum = enabledNotchIndexes().first.flatMap { 283 configuration.heightForNotch(at: $0) 284 } ?? configuration.maximumNotchHeight 285 // (gz) 2019-04-11 If the overlay is still at a disabled notch 286 return min(translationEndNotchHeight, minimum) 287 } 288 289 private func maximumReachableNotchHeight() -> CGFloat { 290 let maximum = enabledNotchIndexes().last.flatMap { 291 configuration.heightForNotch(at: $0) 292 } ?? configuration.maximumNotchHeight 293 // (gz) 2019-04-11 If the overlay is still at a disabled notch 294 return max(translationEndNotchHeight, maximum) 295 } 296 } 297