// // BottomSheetAnimationViewController.swift // import UIKit // MARK: BottomSheetView final class BottomSheetView: UIView { private let handleViewHeight: CGFloat = 8 private lazy var handleView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .tertiarySystemBackground view.layer.cornerRadius = handleViewHeight / 2 view.clipsToBounds = true return view }() init() { super.init(frame: .zero) addSubview(handleView) NSLayoutConstraint.activate([ handleView.centerXAnchor.constraint(equalTo: centerXAnchor), handleView.centerYAnchor.constraint(equalTo: topAnchor, constant: handleViewHeight), handleView.heightAnchor.constraint(equalToConstant: handleViewHeight), handleView.widthAnchor.constraint(equalToConstant: handleViewHeight * 10) ]) backgroundColor = .secondarySystemBackground layer.cornerRadius = 16 layer.maskedCorners = [ .layerMinXMinYCorner, .layerMaxXMinYCorner ] clipsToBounds = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: BottomSheetAnimationViewController class BottomSheetAnimationViewController: UIViewController { private lazy var bottomSheetView: UIView = { let bottomSheetView = BottomSheetView() bottomSheetView.translatesAutoresizingMaskIntoConstraints = false return bottomSheetView }() private lazy var panGestureRecognizer = UIPanGestureRecognizer( target: self, action: #selector(onPan) ) private let animator: BottomSheetAnimator = BottomSheetAnimator() override func loadView() { let view = UIView() view.addSubview(bottomSheetView) let bottomConstraint = bottomSheetView.bottomAnchor.constraint( equalTo: view.bottomAnchor ) animator.bottomConstraint = bottomConstraint NSLayoutConstraint.activate([ bottomSheetView.heightAnchor.constraint( equalTo: view.heightAnchor, multiplier: 0.66 ), bottomSheetView.leadingAnchor.constraint(equalTo: view.leadingAnchor), bottomSheetView.trailingAnchor.constraint(equalTo: view.trailingAnchor), bottomConstraint, ]) view.backgroundColor = .systemBackground self.view = view } override func viewDidLoad() { super.viewDidLoad() bottomSheetView.addGestureRecognizer(panGestureRecognizer) } @objc private func onPan(_ panGestureRecognizer: UIPanGestureRecognizer) { guard let view = panGestureRecognizer.view else { return } let translation = panGestureRecognizer.translation(in: view.superview) switch panGestureRecognizer.state { case .possible: break case .failed: break case .began: animator.start(animating: view) case .changed: animator.move(view, basedOn: translation) default: // .ended, .cancelled, @unknown let velocity = panGestureRecognizer.velocity(in: view.superview) animator.stop(animating: view, with: velocity) } } } // MARK: BottomSheetAnimator class BottomSheetAnimator { var bottomConstraint: NSLayoutConstraint? // Capture the sheet's initial height. private var initialSheetHeight: CGFloat = .zero private var offsetAnimator: UIViewPropertyAnimator? private func makeOffsetAnimator( animating view: UIView, to offset: CGFloat ) -> UIViewPropertyAnimator { let propertyAnimator = UIViewPropertyAnimator( duration: 0.25, // Low values makes the scrubbing snap between values at the start. dampingRatio: 1 ) propertyAnimator.addAnimations { self.bottomConstraint?.constant = offset view.superview?.layoutIfNeeded() } propertyAnimator.addCompletion { position in self.bottomConstraint?.constant = position == .end ? offset : 0 } return propertyAnimator } } extension BottomSheetAnimator { func start(animating view: UIView) { initialSheetHeight = view.frame.height offsetAnimator = makeOffsetAnimator( animating: view, to: initialSheetHeight ) } func move(_ view: UIView, basedOn translation: CGPoint) { let fractionComplete = min(max(translation.y, 0) / initialSheetHeight, 1) offsetAnimator?.fractionComplete = fractionComplete } func stop(animating view: UIView, with velocity: CGPoint) { let fractionComplete = offsetAnimator?.fractionComplete ?? 0 offsetAnimator?.isReversed = fractionComplete < 0.5 offsetAnimator?.continueAnimation( withTimingParameters: nil, durationFactor: 1 ) } }