Forked from vinczebalazs/SheetModalPresentationController.swift
Created
August 17, 2022 17:44
-
-
Save V8tr/bb4157dbab3f14b2f96cb8b51a44a6e2 to your computer and use it in GitHub Desktop.
Revisions
-
vinczebalazs revised this gist
Oct 11, 2020 . 1 changed file with 242 additions and 105 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,101 +1,229 @@ import UIKit extension UIView { var allSubviews: [UIView] { subviews + subviews.flatMap { $0.allSubviews } } func firstSubview<T: UIView>(of type: T.Type) -> T? { allSubviews.first { $0 is T } as? T } func findFirstResponder() -> UIView? { for subview in subviews { if subview.isFirstResponder { return subview } if let recursiveSubView = subview.findFirstResponder() { return recursiveSubView } } return nil } } extension UIApplication { static var statusBarHeight: CGFloat { Self.shared.statusBarFrame.height } } extension UIViewController { /// Return the preferred height of the view controller, taking scrollviews into account. var preferredHeight: CGFloat { /// If the view controller provides it's own preferred size, use it. if preferredContentSize.height > 0 { return preferredContentSize.height } return calculatePreferredHeight() } func calculatePreferredHeight() -> CGFloat { // Insets are all zero intially, but once setup they will influence the results of the systemLayoutSizeFitting method. let insets = view.safeAreaInsets.top + view.safeAreaInsets.bottom // We substract the insets from the height to always get the actual height of only the view itself. var height = max(0, view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height - insets) // Support for UITableViewControllers. if let tableView = view as? UITableView { height += tableView.contentSize.height + tableView.contentInset.top + tableView.contentInset.bottom return height } // Include scroll views in the height calculation. height += view.subviews.filter { $0 is UIScrollView }.reduce(CGFloat(0), { result, view in if view.intrinsicContentSize.height <= 0 { // If a scroll view does not have an intrinsic content size set, use the content size. let scrollView = view as! UIScrollView return result + scrollView.contentSize.height + scrollView.contentInset.top + scrollView.contentInset.bottom } else { return result } }) return height } func requestHeightUpdate() { // Set the preferredContentSize to force a preferredContentSizeDidChange call in the parent. preferredContentSize.height += 1 preferredContentSize.height = 0 } func presentAsSheet(_ vc: UIViewController, isDismissable: Bool) { let presentationController = SheetModalPresentationController(presentedViewController: vc, presenting: self, isDismissable: isDismissable) vc.transitioningDelegate = presentationController vc.modalPresentationStyle = .custom present(vc, animated: true) } } final class SheetModalPresentationController: UIPresentationController { // MARK: Private Properties private let isDismissable: Bool private let interactor = UIPercentDrivenInteractiveTransition() private let dimmingView = UIView() private var propertyAnimator: UIViewPropertyAnimator! private var isInteractive = false private var navigationController: UINavigationController { presentedViewController as! UINavigationController } private var nestedViewController: UIViewController? { navigationController.viewControllers.last } private var scrollView: UIScrollView? { nestedViewController?.view as? UIScrollView ?? nestedViewController?.view.firstSubview(of: UIScrollView.self) } private let topOffset = UIApplication.statusBarHeight + 20 // MARK: Public Properties override var frameOfPresentedViewInContainerView: CGRect { guard let containerBounds = containerView?.bounds, let nestedViewController = nestedViewController, let window = UIApplication.shared.keyWindow else { return .zero } let extraPadding = navigationController.navigationBar.frame.height + navigationController.additionalSafeAreaInsets.top + window.safeAreaInsets.bottom + nestedViewController.additionalSafeAreaInsets.top + nestedViewController.additionalSafeAreaInsets.bottom var frame = containerBounds frame.size.height = min(nestedViewController.preferredHeight + extraPadding, containerBounds.height - topOffset) frame.origin.y = containerBounds.height - frame.size.height return frame } // MARK: Initializers init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, isDismissable: Bool) { self.isDismissable = isDismissable super.init(presentedViewController: presentedViewController, presenting: presentingViewController) registerForKeyboardNotifications() } // MARK: Public Methods override func presentationTransitionWillBegin() { guard let containerBounds = containerView?.bounds, let presentedView = presentedView else { return } // Configure the presented view. containerView?.addSubview(presentedView) presentedView.layoutIfNeeded() presentedView.frame = frameOfPresentedViewInContainerView presentedView.frame.origin.y = containerBounds.height presentedView.layer.masksToBounds = true presentedView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] presentedView.layer.cornerRadius = 40 // Add a dimming view below the presented view controller. dimmingView.backgroundColor = .black dimmingView.frame = containerBounds dimmingView.alpha = 0 containerView?.insertSubview(dimmingView, at: 0) // Add pan gesture recognizers for interactive dismissal. presentedView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) scrollView?.panGestureRecognizer.addTarget(self, action: #selector(handlePan(_:))) // Add tap recognizer for sheet and keyboard dismissal. dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleDismiss))) presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [unowned self] _ in self.dimmingView.alpha = 0.5 }) } override func dismissalTransitionWillBegin() { presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [unowned self] _ in self.dimmingView.alpha = 0 }) } override func dismissalTransitionDidEnd(_ completed: Bool) { // Not setting this to nil causes a retain cycle for some reason. propertyAnimator = nil } override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { super.preferredContentSizeDidChange(forChildContentContainer: container) if propertyAnimator != nil && !propertyAnimator.isRunning { presentedView?.frame = frameOfPresentedViewInContainerView presentedView?.layoutIfNeeded() } } // MARK: Private Methods @objc private func handleDismiss() { presentedView?.endEditing(true) if isDismissable { presentedViewController.dismiss(animated: true) } } @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { guard isDismissable, let containerView = containerView else { return } limitScrollView(gesture) let percent = gesture.translation(in: containerView).y / containerView.bounds.height switch gesture.state { case .began: if !presentedViewController.isBeingDismissed && scrollView?.contentOffset.y ?? 0 <= 0 { isInteractive = true presentedViewController.dismiss(animated: true) } case .changed: interactor.update(percent) case .cancelled: interactor.cancel() isInteractive = false case .ended: let velocity = gesture.velocity(in: containerView).y interactor.completionSpeed = 0.9 if percent > 0.3 || velocity > 1600 { interactor.finish() } else { interactor.cancel() @@ -106,12 +234,53 @@ final class SheetModalPresentationController: UIPresentationController { } } private func limitScrollView(_ gesture: UIPanGestureRecognizer) { guard let scrollView = scrollView else { return } if interactor.percentComplete > 0 { // Don't let the scroll view scroll while dismissing. scrollView.contentOffset.y = -scrollView.adjustedContentInset.top } } /// Handle the keyboard. private func registerForKeyboardNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardToggled(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardToggled(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) } @objc private func keyboardToggled(notification: NSNotification) { guard let containerHeight = containerView?.bounds.height, let presentedView = presentedView, let textInput = presentedView.findFirstResponder(), let textInputFrame = textInput.superview?.convert(textInput.frame, to: presentedView.superview) else { return assertionFailure() } // Adjust the presented view to move the active text input out of the keyboards's way (if needed). if notification.name == UIResponder.keyboardWillShowNotification { guard let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return assertionFailure() } let keyboardOverlap = textInputFrame.maxY - keyboardFrame.minY + 20 if keyboardOverlap > 0 { presentedView.frame.origin.y = max(presentedView.frame.minY - keyboardOverlap, topOffset) } } else if notification.name == UIResponder.keyboardWillHideNotification { presentedView.frame.origin.y = containerHeight - presentedView.frame.size.height } } } // MARK: UIViewControllerAnimatedTransitioning extension SheetModalPresentationController: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 0.5 } @@ -122,24 +291,34 @@ extension SheetModalPresentationController: UIViewControllerAnimatedTransitionin func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx: 1, dy: 1))) propertyAnimator.addAnimations { [unowned self] in if self.presentedViewController.isBeingPresented { transitionContext.view(forKey: .to)?.frame = self.frameOfPresentedViewInContainerView } else { transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY } } propertyAnimator.addCompletion { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } return propertyAnimator } } // MARK: UIViewControllerTransitioningDelegate extension SheetModalPresentationController: UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { self } func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { self } @@ -151,46 +330,4 @@ extension SheetModalPresentationController: UIViewControllerTransitioningDelegat isInteractive ? interactor : nil } } -
vinczebalazs revised this gist
Jun 17, 2020 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -55,15 +55,15 @@ final class SheetModalPresentationController: UIPresentationController { dimmingView.alpha = 0 containerView?.insertSubview(dimmingView, at: 0) presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [unowned self] in self.dimmingView.alpha = 0.5 }) } override func dismissalTransitionWillBegin() { super.dismissalTransitionWillBegin() presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [unowned self] in self.dimmingView.alpha = 0 }) } @@ -123,7 +123,7 @@ extension SheetModalPresentationController: UIViewControllerAnimatedTransitionin func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters()) propertyAnimator.addAnimations { _ in // Move the view down. transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY } -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -93,7 +93,7 @@ final class SheetModalPresentationController: UIPresentationController { let velocity = gesture.velocity(in: gestureView).y // Finish the animation if the user flicked the modal quickly (i.e. high velocity), or dragged it more than 50% down. if percent > 0.5 || velocity > 1600 { // Multiply the animation duration by the velocity, to make sure the modal dismisses as fast as the user swiped. // If the user pulled down slowly though, we want to use the default duration, hence the max(). interactor.completionSpeed = max(1, velocity / (gestureView.frame.height * (1 / interactor.duration))) interactor.finish() -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 4 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -27,7 +27,6 @@ final class SheetModalPresentationController: UIPresentationController { } // MARK: Function Overrides override var frameOfPresentedViewInContainerView: CGRect { guard let containerBounds = containerView?.bounds else { return .zero } @@ -80,7 +79,6 @@ final class SheetModalPresentationController: UIPresentationController { gesture.translation(in: gestureView).y >= 0 else { return } // Make sure we only recognize downward gestures. let percent = gesture.translation(in: gestureView).y / gestureView.bounds.height switch gesture.state { case .began: @@ -92,11 +90,12 @@ final class SheetModalPresentationController: UIPresentationController { interactor.cancel() isInteractive = false case .ended: let velocity = gesture.velocity(in: gestureView).y // Finish the animation if the user flicked the modal quickly (i.e. high velocity), or dragged it more than 50% down. if percent > 0.5 || velocity > 1600 { // Multiply the animation duration by the velocity, to make sure the modal dismiss as fast as the user swiped. // If the user pulled down slowly though, we want to use the default duration, hence the max(). interactor.completionSpeed = max(1, velocity / (gestureView.frame.height * (1 / interactor.duration))) interactor.finish() } else { interactor.cancel() -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -96,7 +96,7 @@ final class SheetModalPresentationController: UIPresentationController { if percent > 0.5 || velocityY > 1300 { // Multiply the animation duration by the velocity, to make sure the modal dismiss as fast as the user swiped. // If the user pulled down slowly though, we want to use the default duration, hence the max(). interactor.completionSpeed = max(1, velocityY / (gestureView.frame.height * (1 / interactor.duration))) interactor.finish() } else { interactor.cancel() -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -96,7 +96,7 @@ final class SheetModalPresentationController: UIPresentationController { if percent > 0.5 || velocityY > 1300 { // Multiply the animation duration by the velocity, to make sure the modal dismiss as fast as the user swiped. // If the user pulled down slowly though, we want to use the default duration, hence the max(). interactor.completionSpeed = max(1, velocity / (gestureView.frame.height * (1 / interactor.duration))) interactor.finish() } else { interactor.cancel() -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 1 addition and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -92,8 +92,7 @@ final class SheetModalPresentationController: UIPresentationController { interactor.cancel() isInteractive = false case .ended: // Finish the animation if the user flicked the modal quickly (i.e. high velocity), or dragged it more than 50% down. if percent > 0.5 || velocityY > 1300 { // Multiply the animation duration by the velocity, to make sure the modal dismiss as fast as the user swiped. // If the user pulled down slowly though, we want to use the default duration, hence the max(). -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 27 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -169,3 +169,30 @@ extension UIViewController { } } // Example usage: final class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(presentPressed))) } @objc private func presentPressed() { presentAsSheet(ModalViewController(), height: 600) } } final class ModalViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .blue } } -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 29 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -14,11 +14,7 @@ final class SheetModalPresentationController: UIPresentationController { private let height: CGFloat private let interactor = UIPercentDrivenInteractiveTransition() private let dimmingView = UIView() private var propertyAnimator: UIViewPropertyAnimator! private var isInteractive = false @@ -35,9 +31,11 @@ final class SheetModalPresentationController: UIPresentationController { override var frameOfPresentedViewInContainerView: CGRect { guard let containerBounds = containerView?.bounds else { return .zero } // Calculate the frame for the presented view controller using the passed in height. var frame = containerBounds frame.size.height = height frame.origin.y = containerBounds.height - height return frame } @@ -46,10 +44,14 @@ final class SheetModalPresentationController: UIPresentationController { guard let containerBounds = containerView?.bounds else { return } // Add a pan gesture recognizer for pull to dismiss. presentedView?.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) // Round the the presented view controller's corners. presentedView?.layer.cornerRadius = 40 // Add a dimming view below the presented view controller, and a tap gesture recognizer to it for dismissal. dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismiss))) dimmingView.backgroundColor = .black dimmingView.frame = containerBounds dimmingView.alpha = 0 containerView?.insertSubview(dimmingView, at: 0) @@ -74,7 +76,8 @@ final class SheetModalPresentationController: UIPresentationController { } @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { guard let gestureView = gesture.view, gesture.translation(in: gestureView).y >= 0 else { return } // Make sure we only recognize downward gestures. let percent = gesture.translation(in: gestureView).y / gestureView.bounds.height let velocityY = gesture.velocity(in: gestureView).y @@ -89,7 +92,11 @@ final class SheetModalPresentationController: UIPresentationController { interactor.cancel() isInteractive = false case .ended: // Finish the animation if the user flicked the modal quickly (i.e. high velocity), or dragged it // more than 50% percent down. if percent > 0.5 || velocityY > 1300 { // Multiply the animation duration by the velocity, to make sure the modal dismiss as fast as the user swiped. // If the user pulled down slowly though, we want to use the default duration, hence the max(). interactor.completionSpeed = max(1, (gestureView.frame.height * (1 / interactor.duration) / velocityY)) interactor.finish() } else { @@ -119,6 +126,7 @@ extension SheetModalPresentationController: UIViewControllerAnimatedTransitionin propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters()) propertyAnimator.addAnimations { // Move the view down. transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY } propertyAnimator.addCompletion { _ in @@ -146,3 +154,18 @@ extension SheetModalPresentationController: UIViewControllerTransitioningDelegat } } // MARK: UIViewController Extension extension UIViewController { func presentAsSheet(_ vc: UIViewController, height: CGFloat) { let presentationController = SheetModalPresentationController(presentedViewController: vc, presenting: self, height: height) vc.transitioningDelegate = presentationController vc.modalPresentationStyle = .custom present(vc, animated: true) } } -
vinczebalazs revised this gist
Jan 19, 2020 . 1 changed file with 23 additions and 55 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -12,15 +12,15 @@ final class SheetModalPresentationController: UIPresentationController { // MARK: Private Properties private let height: CGFloat private let interactor = UIPercentDrivenInteractiveTransition() private lazy var dimmingView: UIView = { let dimmingView = UIView() dimmingView.backgroundColor = .black return dimmingView }() private var propertyAnimator: UIViewPropertyAnimator! private var isInteractive = false // MARK: Initializers @@ -81,7 +81,6 @@ final class SheetModalPresentationController: UIPresentationController { switch gesture.state { case .began: isInteractive = true presentedViewController.dismiss(animated: true, completion: nil) case .changed: @@ -91,8 +90,6 @@ final class SheetModalPresentationController: UIPresentationController { isInteractive = false case .ended: if percent > 0.5 || velocityY > 1300 { interactor.completionSpeed = max(1, (gestureView.frame.height * (1 / interactor.duration) / velocityY)) interactor.finish() } else { @@ -106,70 +103,23 @@ final class SheetModalPresentationController: UIPresentationController { } // MARK: UIViewControllerAnimatedTransitioning extension SheetModalPresentationController: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 0.5 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { interruptibleAnimator(using: transitionContext).startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters()) propertyAnimator.addAnimations { transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY } propertyAnimator.addCompletion { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) @@ -178,3 +128,21 @@ extension SheetTransitionAnimator: UIViewControllerAnimatedTransitioning { } } // MARK: UIViewControllerTransitioningDelegate extension SheetModalPresentationController: UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { self } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { self } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { isInteractive ? interactor : nil } } -
vinczebalazs created this gist
Jan 19, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,180 @@ // // SheetModalPresentationController.swift // // // Created by Balazs Vincze on 2020. 01. 14.. // Copyright © 2020. Balazs Vincze. All rights reserved. // import UIKit final class SheetModalPresentationController: UIPresentationController { // MARK: Private Properties private lazy var dimmingView: UIView = { let dimmingView = UIView() dimmingView.backgroundColor = .black return dimmingView }() private let interactor = UIPercentDrivenInteractiveTransition() private var isInteractive = false private let height: CGFloat private var gestureStart: Date! // MARK: Initializers init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, height: CGFloat) { self.height = min(height, presentedViewController.view.frame.height) super.init(presentedViewController: presentedViewController, presenting: presentingViewController) } // MARK: Function Overrides override var frameOfPresentedViewInContainerView: CGRect { guard let containerBounds = containerView?.bounds else { return .zero } var frame = containerBounds frame.size.height = height frame.origin.y = containerBounds.height - height return frame } override func presentationTransitionWillBegin() { super.presentationTransitionWillBegin() guard let containerBounds = containerView?.bounds else { return } presentedView?.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) presentedView?.layer.cornerRadius = 40 dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismiss))) dimmingView.frame = containerBounds dimmingView.alpha = 0 containerView?.insertSubview(dimmingView, at: 0) presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in self.dimmingView.alpha = 0.5 }) } override func dismissalTransitionWillBegin() { super.dismissalTransitionWillBegin() presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in self.dimmingView.alpha = 0 }) } // MARK: Private Functions @objc private func dismiss() { presentedViewController.dismiss(animated: true) } @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { guard let gestureView = gesture.view, gesture.translation(in: gestureView).y >= 0 else { return } let percent = gesture.translation(in: gestureView).y / gestureView.bounds.height let velocityY = gesture.velocity(in: gestureView).y switch gesture.state { case .began: gestureStart = Date() isInteractive = true presentedViewController.dismiss(animated: true, completion: nil) case .changed: interactor.update(percent) case .cancelled: interactor.cancel() isInteractive = false case .ended: if percent > 0.5 || velocityY > 1300 { interactor.completionSpeed = max(1, (gestureView.frame.height * (1 / interactor.duration) / velocity.y)) interactor.completionSpeed = max(1, (gestureView.frame.height * (1 / interactor.duration) / velocityY)) interactor.finish() } else { interactor.cancel() } isInteractive = false default: break } } } // MARK: UIViewControllerTransitioningDelegate extension SheetPresentationController: UIViewControllerTransitioningDelegate { func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { self } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { SheetTransitionAnimator(.dismissed) } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { isInteractive ? interactor : nil } } final class SheetTransitionAnimator: NSObject { enum TransitionType { case presented case dismissed } private let type: TransitionType private var propertyAnimator: UIViewPropertyAnimator! init(_ type: TransitionType) { self.type = type super.init() } } // MARK: UIViewControllerAnimatedTransitioning extension SheetTransitionAnimator: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 0.5 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if type == .presented, let toView = transitionContext.view(forKey: .to) { transitionContext.containerView.addSubview(toView) toView.frame.origin.y = transitionContext.containerView.frame.maxY } interruptibleAnimator(using: transitionContext).startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: UICubicTimingParameters()) propertyAnimator.addAnimations { switch self.type { case .presented: guard let toVC = transitionContext.viewController(forKey: .to) else { break } toVC.view.frame = transitionContext.finalFrame(for: toVC) case .dismissed: transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY } } propertyAnimator.addCompletion { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } return propertyAnimator } }