115 lines
3.7 KiB
Swift
115 lines
3.7 KiB
Swift
|
|
final class ModalPresentationStepsController {
|
||
|
|
|
||
|
|
enum StepUpdate {
|
||
|
|
case didClose
|
||
|
|
case didUpdateFrame(CGRect)
|
||
|
|
case didUpdateStep(ModalPresentationStep)
|
||
|
|
}
|
||
|
|
|
||
|
|
fileprivate enum Constants {
|
||
|
|
static let slowSwipeVelocity: CGFloat = 500
|
||
|
|
static let fastSwipeDownVelocity: CGFloat = 4000
|
||
|
|
static let fastSwipeUpVelocity: CGFloat = 3000
|
||
|
|
static let translationThreshold: CGFloat = 50
|
||
|
|
}
|
||
|
|
|
||
|
|
private weak var presentedView: UIView?
|
||
|
|
private weak var containerViewController: UIViewController?
|
||
|
|
|
||
|
|
private var initialTranslationY: CGFloat = .zero
|
||
|
|
|
||
|
|
private(set) var currentStep: ModalPresentationStep = .fullScreen
|
||
|
|
private(set) var maxAvailableFrame: CGRect = .zero
|
||
|
|
|
||
|
|
var currentFrame: CGRect { frame(for: currentStep) }
|
||
|
|
var hiddenFrame: CGRect { frame(for: .hidden) }
|
||
|
|
|
||
|
|
var didUpdateHandler: ((StepUpdate) -> Void)?
|
||
|
|
|
||
|
|
func set(presentedView: UIView, containerViewController: UIViewController) {
|
||
|
|
self.presentedView = presentedView
|
||
|
|
self.containerViewController = containerViewController
|
||
|
|
}
|
||
|
|
|
||
|
|
func setInitialState() {
|
||
|
|
setStep(.hidden, animation: .none)
|
||
|
|
}
|
||
|
|
|
||
|
|
func close(completion: (() -> Void)? = nil) {
|
||
|
|
setStep(.hidden, animation: .slide, completion: completion)
|
||
|
|
}
|
||
|
|
|
||
|
|
func updateMaxAvailableFrame() {
|
||
|
|
maxAvailableFrame = frame(for: .fullScreen)
|
||
|
|
}
|
||
|
|
|
||
|
|
func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||
|
|
guard let presentedView else { return }
|
||
|
|
let translation = gesture.translation(in: presentedView)
|
||
|
|
let velocity = gesture.velocity(in: presentedView)
|
||
|
|
var currentFrame = presentedView.frame
|
||
|
|
|
||
|
|
switch gesture.state {
|
||
|
|
case .began:
|
||
|
|
initialTranslationY = presentedView.frame.origin.y
|
||
|
|
case .changed:
|
||
|
|
let newY = max(max(initialTranslationY + translation.y, 0), maxAvailableFrame.origin.y)
|
||
|
|
currentFrame.origin.y = newY
|
||
|
|
presentedView.frame = currentFrame
|
||
|
|
didUpdateHandler?(.didUpdateFrame(currentFrame))
|
||
|
|
case .ended:
|
||
|
|
let nextStep: ModalPresentationStep
|
||
|
|
if velocity.y > Constants.fastSwipeDownVelocity {
|
||
|
|
didUpdateHandler?(.didClose)
|
||
|
|
return
|
||
|
|
} else if velocity.y < -Constants.fastSwipeUpVelocity {
|
||
|
|
nextStep = .fullScreen
|
||
|
|
} else if velocity.y > Constants.slowSwipeVelocity || translation.y > Constants.translationThreshold {
|
||
|
|
if currentStep == .compact {
|
||
|
|
didUpdateHandler?(.didClose)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
nextStep = currentStep.lower
|
||
|
|
} else if velocity.y < -Constants.slowSwipeVelocity || translation.y < -Constants.translationThreshold {
|
||
|
|
nextStep = currentStep.upper
|
||
|
|
} else {
|
||
|
|
nextStep = currentStep
|
||
|
|
}
|
||
|
|
|
||
|
|
let animation: PresentationStepChangeAnimation = abs(velocity.y) > Constants.slowSwipeVelocity ? .slideAndBounce : .slide
|
||
|
|
setStep(nextStep, animation: animation)
|
||
|
|
default:
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func setStep(_ step: ModalPresentationStep,
|
||
|
|
completion: (() -> Void)? = nil) {
|
||
|
|
guard currentStep != step else { return }
|
||
|
|
setStep(step, animation: .slide, completion: completion)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func setStep(_ step: ModalPresentationStep,
|
||
|
|
animation: PresentationStepChangeAnimation,
|
||
|
|
completion: (() -> Void)? = nil) {
|
||
|
|
guard let presentedView else { return }
|
||
|
|
currentStep = step
|
||
|
|
updateMaxAvailableFrame()
|
||
|
|
|
||
|
|
let frame = frame(for: step)
|
||
|
|
didUpdateHandler?(.didUpdateStep(step))
|
||
|
|
didUpdateHandler?(.didUpdateFrame(frame))
|
||
|
|
|
||
|
|
ModalPresentationAnimator.animate(with: animation) {
|
||
|
|
presentedView.frame = frame
|
||
|
|
} completion: { _ in
|
||
|
|
completion?()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func frame(for step: ModalPresentationStep) -> CGRect {
|
||
|
|
guard let presentedView, let containerViewController else { return .zero }
|
||
|
|
return step.frame(for: presentedView, in: containerViewController)
|
||
|
|
}
|
||
|
|
}
|