Last active
August 30, 2022 07:37
-
-
Save pronebird/39fd4dae64d363cbf3af01c64121f474 to your computer and use it in GitHub Desktop.
Custom formsheet presentation
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 characters
class FormsheetPresentationController: UIPresentationController { | |
private static let dimmingViewOpacityWhenPresented = 0.5 | |
private var isPresented = false | |
private let dimmingView: UIView = { | |
let dimmingView = UIView() | |
dimmingView.backgroundColor = .black | |
return dimmingView | |
}() | |
override var shouldRemovePresentersView: Bool { | |
return false | |
} | |
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { | |
super.viewWillTransition(to: size, with: coordinator) | |
coordinator.animate { context in | |
guard let containerView = self.containerView, self.isPresented else { return } | |
let targetFrame = FormsheetPresentationAnimator.targetFrame( | |
in: containerView, | |
preferredContentSize: self.presentedViewController.preferredContentSize | |
) | |
self.presentedView?.frame = targetFrame | |
} | |
} | |
override func containerViewWillLayoutSubviews() { | |
dimmingView.frame = containerView?.bounds ?? .zero | |
} | |
override func presentationTransitionWillBegin() { | |
dimmingView.alpha = 0 | |
containerView?.addSubview(dimmingView) | |
if let transitionCoordinator = presentingViewController.transitionCoordinator { | |
transitionCoordinator.animate { context in | |
self.dimmingView.alpha = Self.dimmingViewOpacityWhenPresented | |
} | |
} else { | |
self.dimmingView.alpha = Self.dimmingViewOpacityWhenPresented | |
} | |
} | |
override func presentationTransitionDidEnd(_ completed: Bool) { | |
if completed { | |
isPresented = true | |
} else { | |
dimmingView.removeFromSuperview() | |
} | |
} | |
override func dismissalTransitionWillBegin() { | |
presentingViewController.transitionCoordinator?.animate { context in | |
self.dimmingView.alpha = 0 | |
} | |
} | |
override func dismissalTransitionDidEnd(_ completed: Bool) { | |
if completed { | |
dimmingView.removeFromSuperview() | |
isPresented = false | |
} | |
} | |
} | |
class FormsheetTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { | |
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return FormsheetPresentationAnimator() | |
} | |
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return FormsheetPresentationAnimator() | |
} | |
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { | |
return FormsheetPresentationController(presentedViewController: presented, presenting: source) | |
} | |
} | |
class FormsheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning { | |
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { | |
return (transitionContext?.isAnimated ?? true) ? 0.5 : 0 | |
} | |
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
let destination = transitionContext.viewController(forKey: .to) | |
if destination?.isBeingPresented ?? false { | |
animatePresentation(transitionContext) | |
} else { | |
animateDismissal(transitionContext) | |
} | |
} | |
private func animatePresentation(_ transitionContext: UIViewControllerContextTransitioning) { | |
let duration = transitionDuration(using: transitionContext) | |
let containerView = transitionContext.containerView | |
let destinationView = transitionContext.view(forKey: .to)! | |
let destinationController = transitionContext.viewController(forKey: .to)! | |
let preferredContentSize = destinationController.preferredContentSize | |
containerView.addSubview(destinationView) | |
destinationView.frame = Self.initialFrame( | |
in: containerView, | |
preferredContentSize: preferredContentSize | |
) | |
UIView.animate( | |
withDuration: duration, | |
delay: 0, | |
options: [.curveEaseInOut], | |
animations: { | |
destinationView.frame = Self.targetFrame( | |
in: containerView, | |
preferredContentSize: preferredContentSize | |
) | |
}, | |
completion: { _ in | |
transitionContext.completeTransition(true) | |
}) | |
} | |
private func animateDismissal(_ transitionContext: UIViewControllerContextTransitioning) { | |
let duration = transitionDuration(using: transitionContext) | |
let containerView = transitionContext.containerView | |
let sourceView = transitionContext.view(forKey: .from)! | |
let sourceController = transitionContext.viewController(forKey: .from)! | |
let preferredContentSize = sourceController.preferredContentSize | |
UIView.animate( | |
withDuration: duration, | |
delay: 0, | |
options: [.curveEaseInOut], | |
animations: { | |
sourceView.frame = Self.initialFrame( | |
in: containerView, | |
preferredContentSize: preferredContentSize | |
) | |
}, | |
completion: { _ in | |
transitionContext.completeTransition(true) | |
}) | |
} | |
fileprivate static func initialFrame(in containerView: UIView, preferredContentSize: CGSize) -> CGRect { | |
assert(preferredContentSize.width > 0 && preferredContentSize.height > 0) | |
return CGRect( | |
origin: CGPoint( | |
x: containerView.bounds.midX - preferredContentSize.width * 0.5, | |
y: containerView.bounds.maxY | |
), | |
size: preferredContentSize | |
) | |
} | |
fileprivate static func targetFrame(in containerView: UIView, preferredContentSize: CGSize) -> CGRect { | |
assert(preferredContentSize.width > 0 && preferredContentSize.height > 0) | |
return CGRect( | |
origin: CGPoint( | |
x: containerView.bounds.midX - preferredContentSize.width * 0.5, | |
y: containerView.bounds.midY - preferredContentSize.height * 0.5 | |
), | |
size: preferredContentSize | |
) | |
} | |
} | |
// USAGE: | |
// Store somewhere for the duration of transition, i.e in presenting class | |
let formsheetTransitioningDelegate = FormsheetTransitioningDelegate() | |
// Present with custom modal transition | |
let viewController = UIViewController() | |
viewController.view.backgroundColor = .systemMint | |
let navigationController = UINavigationController(rootViewController: viewController) | |
navigationController.transitioningDelegate = formsheetTransitioningDelegate | |
navigationController.modalPresentationStyle = .custom | |
navigationController.preferredContentSize = CGSize(width: 320, height: 240) | |
navigationController.view.layer.cornerRadius = 8 | |
present(navigationController, animated: true) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment