Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -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()
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}