Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
|
|
@ -0,0 +1,49 @@
|
|||
enum PresentationStepChangeAnimation {
|
||||
case none
|
||||
case slide
|
||||
case slideAndBounce
|
||||
}
|
||||
|
||||
final class ModalPresentationAnimator {
|
||||
|
||||
private enum Constants {
|
||||
static let animationDuration: TimeInterval = kDefaultAnimationDuration
|
||||
static let springDamping: CGFloat = 0.8
|
||||
static let springVelocity: CGFloat = 0.2
|
||||
static let controlPoint1: CGPoint = CGPoint(x: 0.25, y: 0.1)
|
||||
static let controlPoint2: CGPoint = CGPoint(x: 0.15, y: 1.0)
|
||||
}
|
||||
|
||||
static func animate(with stepAnimation: PresentationStepChangeAnimation = .slide,
|
||||
animations: @escaping (() -> Void),
|
||||
completion: ((Bool) -> Void)?) {
|
||||
switch stepAnimation {
|
||||
case .none:
|
||||
animations()
|
||||
completion?(true)
|
||||
|
||||
case .slide:
|
||||
let timing = UICubicTimingParameters(controlPoint1: Constants.controlPoint1,
|
||||
controlPoint2: Constants.controlPoint2)
|
||||
let animator = UIViewPropertyAnimator(duration: Constants.animationDuration,
|
||||
timingParameters: timing)
|
||||
animator.addAnimations(animations)
|
||||
animator.addCompletion { position in
|
||||
completion?(position == .end)
|
||||
}
|
||||
animator.startAnimation()
|
||||
|
||||
case .slideAndBounce:
|
||||
let velocity = CGVector(dx: Constants.springVelocity, dy: Constants.springVelocity)
|
||||
let timing = UISpringTimingParameters(dampingRatio: Constants.springDamping,
|
||||
initialVelocity: velocity)
|
||||
let animator = UIViewPropertyAnimator(duration: Constants.animationDuration,
|
||||
timingParameters: timing)
|
||||
animator.addAnimations(animations)
|
||||
animator.addCompletion { position in
|
||||
completion?(position == .end)
|
||||
}
|
||||
animator.startAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
enum ModalPresentationStep: Int, CaseIterable {
|
||||
case fullScreen
|
||||
case halfScreen
|
||||
case compact
|
||||
case hidden
|
||||
}
|
||||
|
||||
extension ModalPresentationStep {
|
||||
private enum Constants {
|
||||
static let iPadWidth: CGFloat = 350
|
||||
static let compactHeightOffset: CGFloat = 120
|
||||
static let halfScreenHeightFactorPortrait: CGFloat = 0.55
|
||||
static let topInset: CGFloat = 8
|
||||
}
|
||||
|
||||
var upper: ModalPresentationStep {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
return .fullScreen
|
||||
case .halfScreen:
|
||||
return .fullScreen
|
||||
case .compact:
|
||||
return .halfScreen
|
||||
case .hidden:
|
||||
return .compact
|
||||
}
|
||||
}
|
||||
|
||||
var lower: ModalPresentationStep {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
return .halfScreen
|
||||
case .halfScreen:
|
||||
return .compact
|
||||
case .compact:
|
||||
return .compact
|
||||
case .hidden:
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
|
||||
var first: ModalPresentationStep {
|
||||
.fullScreen
|
||||
}
|
||||
|
||||
var last: ModalPresentationStep {
|
||||
.compact
|
||||
}
|
||||
|
||||
func frame(for presentedView: UIView, in containerViewController: UIViewController) -> CGRect {
|
||||
let isIPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||
var containerSize = containerViewController.view.bounds.size
|
||||
if containerSize == .zero {
|
||||
containerSize = UIScreen.main.bounds.size
|
||||
}
|
||||
let safeAreaInsets = containerViewController.view.safeAreaInsets
|
||||
let traitCollection = containerViewController.traitCollection
|
||||
var frame = CGRect(origin: .zero, size: containerSize)
|
||||
|
||||
if isIPad {
|
||||
frame.size.width = Constants.iPadWidth
|
||||
switch self {
|
||||
case .hidden:
|
||||
frame.origin.x = -Constants.iPadWidth
|
||||
default:
|
||||
frame.origin.x = .zero
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
let isPortraitOrientation = traitCollection.verticalSizeClass == .regular
|
||||
if isPortraitOrientation {
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
frame.origin.y = safeAreaInsets.top + Constants.topInset
|
||||
case .halfScreen:
|
||||
frame.origin.y = containerSize.height * Constants.halfScreenHeightFactorPortrait
|
||||
case .compact:
|
||||
frame.origin.y = containerSize.height - Constants.compactHeightOffset
|
||||
case .hidden:
|
||||
frame.origin.y = containerSize.height
|
||||
}
|
||||
} else {
|
||||
frame.size.width = Constants.iPadWidth
|
||||
frame.origin.x = safeAreaInsets.left
|
||||
switch self {
|
||||
case .fullScreen:
|
||||
frame.origin.y = Constants.topInset
|
||||
case .halfScreen, .compact:
|
||||
frame.origin.y = containerSize.height - Constants.compactHeightOffset
|
||||
case .hidden:
|
||||
frame.origin.y = containerSize.height
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue