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