Skip to content

Instantly share code, notes, and snippets.

@pronebird
Last active August 30, 2022 07:37
Show Gist options
  • Save pronebird/39fd4dae64d363cbf3af01c64121f474 to your computer and use it in GitHub Desktop.
Save pronebird/39fd4dae64d363cbf3af01c64121f474 to your computer and use it in GitHub Desktop.
Custom formsheet presentation
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