Created
June 10, 2023 13:26
-
-
Save paigeshin/eb58c10b1794bb1ec84c1791b9d63fb9 to your computer and use it in GitHub Desktop.
This file contains 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
import Foundation | |
import SwiftUI | |
class DialogPresenter: NSObject, UIViewControllerTransitioningDelegate { | |
private var transitionStyle: PopupDialogTransitionStyle = .zoomIn | |
private var popupContentViewController: UIViewController = UIViewController() | |
private var overlayController: PresentationController? | |
override init() { | |
super.init() | |
self.popupContentViewController.modalPresentationStyle = .custom | |
self.popupContentViewController.transitioningDelegate = self | |
self.popupContentViewController.view.backgroundColor = .clear | |
} | |
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { | |
let presentationController = PresentationController(presentedViewController: presented, presenting: source) | |
self.overlayController = presentationController | |
self.overlayController?.onTapOverlay = { [weak self] in | |
self?.dismiss() | |
} | |
return presentationController | |
} | |
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
var transition: TransitionAnimator | |
switch transitionStyle { | |
case .bounceUp: | |
transition = BounceUpTransition(direction: .in) | |
case .bounceDown: | |
transition = BounceDownTransition(direction: .in) | |
case .zoomIn: | |
transition = ZoomTransition(direction: .in) | |
case .fadeIn: | |
transition = FadeTransition(direction: .in) | |
} | |
return transition | |
} | |
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
var transition: TransitionAnimator | |
switch transitionStyle { | |
case .bounceUp: | |
transition = BounceUpTransition(direction: .out) | |
case .bounceDown: | |
transition = BounceDownTransition(direction: .out) | |
case .zoomIn: | |
transition = ZoomTransition(direction: .out) | |
case .fadeIn: | |
transition = FadeTransition(direction: .out) | |
} | |
return transition | |
} | |
func present<Content: View>(style: PopupDialogTransitionStyle = .zoomIn, | |
content: () -> Content) { | |
self.transitionStyle = style | |
self.popupContentViewController = UIHostingController(rootView: content()) | |
self.popupContentViewController.modalPresentationStyle = .custom | |
self.popupContentViewController.transitioningDelegate = self | |
self.popupContentViewController.view.backgroundColor = .clear | |
UIApplication.shared.topMostViewController?.present(self.popupContentViewController, animated: true) | |
} | |
func dismiss() { | |
self.popupContentViewController.dismiss(animated: true) | |
} | |
} | |
extension UIApplication { | |
var keyWindow: UIWindow? { | |
UIApplication | |
.shared | |
.connectedScenes | |
.map({$0 as? UIWindowScene}) | |
.compactMap {$0} | |
.first? | |
.windows | |
.filter { $0.isKeyWindow } | |
.first! | |
} | |
var topMostViewController: UIViewController? { | |
var topMostVC = self.keyWindow?.rootViewController | |
while let presentedVC = topMostVC?.presentedViewController { | |
topMostVC = presentedVC | |
} | |
return topMostVC | |
} | |
} | |
extension View { | |
func fullScreenDialog<Content: View>( | |
isPresented: Binding<Bool>, | |
modalTransitionStyle: UIModalTransitionStyle = .crossDissolve, | |
modalPresentationStyle: UIModalPresentationStyle = .overCurrentContext, | |
backgroundColor: UIColor = .clear, | |
content: () -> Content | |
) -> some View{ | |
let vc = UIHostingController(rootView: content()) | |
vc.modalTransitionStyle = modalTransitionStyle | |
vc.modalPresentationStyle = modalPresentationStyle | |
vc.view.backgroundColor = backgroundColor | |
vc.definesPresentationContext = true | |
return self.onChange(of: isPresented.wrappedValue, perform: { show in | |
if show { | |
UIApplication.shared.topMostViewController?.present(vc, animated: true) | |
} else { | |
UIApplication.shared.topMostViewController?.dismiss(animated: true) | |
} | |
}) | |
} | |
} | |
/*! | |
Presentation transition styles for the popup dialog | |
- BounceUp: Dialog bounces in from bottom and is dismissed to bottom | |
- BounceDown: Dialog bounces in from top and is dismissed to top | |
- ZoomIn: Dialog zooms in and is dismissed by zooming out | |
- FadeIn: Dialog fades in and is dismissed by fading out | |
*/ | |
@objc public enum PopupDialogTransitionStyle: Int { | |
case bounceUp | |
case bounceDown | |
case zoomIn | |
case fadeIn | |
} | |
/// Dialog bounces in from bottom and is dismissed to bottom | |
final internal class BounceUpTransition: TransitionAnimator { | |
init(direction: AnimationDirection) { | |
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction) | |
} | |
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
super.animateTransition(using: transitionContext) | |
switch direction { | |
case .in: | |
to.view.bounds.origin = CGPoint(x: 0, y: -from.view.bounds.size.height) | |
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { [weak self] in | |
guard let self = self else { return } | |
self.to.view.bounds = self.from.view.bounds | |
}, completion: { _ in | |
transitionContext.completeTransition(true) | |
}) | |
case .out: | |
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in | |
guard let self = self else { return } | |
self.from.view.bounds.origin = CGPoint(x: 0, y: -self.from.view.bounds.size.height) | |
self.from.view.alpha = 0.0 | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
}) | |
} | |
} | |
} | |
/// Dialog bounces in from top and is dismissed to top | |
final internal class BounceDownTransition: TransitionAnimator { | |
init(direction: AnimationDirection) { | |
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction) | |
} | |
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
super.animateTransition(using: transitionContext) | |
switch direction { | |
case .in: | |
to.view.bounds.origin = CGPoint(x: 0, y: from.view.bounds.size.height) | |
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { [weak self] in | |
guard let self = self else { return } | |
self.to.view.bounds = self.from.view.bounds | |
}, completion: { _ in | |
transitionContext.completeTransition(true) | |
}) | |
case .out: | |
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in | |
guard let self = self else { return } | |
self.from.view.bounds.origin = CGPoint(x: 0, y: self.from.view.bounds.size.height) | |
self.from.view.alpha = 0.0 | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
}) | |
} | |
} | |
} | |
/// Dialog zooms in and is dismissed by zooming out | |
final internal class ZoomTransition: TransitionAnimator { | |
init(direction: AnimationDirection) { | |
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction) | |
} | |
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
super.animateTransition(using: transitionContext) | |
switch direction { | |
case .in: | |
to.view.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) | |
UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.curveEaseOut], animations: { [weak self] in | |
guard let self = self else { return } | |
self.to.view.transform = CGAffineTransform(scaleX: 1, y: 1) | |
}, completion: { _ in | |
transitionContext.completeTransition(true) | |
}) | |
case .out: | |
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in | |
guard let self = self else { return } | |
self.from.view.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) | |
self.from.view.alpha = 0.0 | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
}) | |
} | |
} | |
} | |
/// Dialog fades in and is dismissed by fading out | |
final internal class FadeTransition: TransitionAnimator { | |
init(direction: AnimationDirection) { | |
super.init(inDuration: 0.22, outDuration: 0.2, direction: direction) | |
} | |
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
super.animateTransition(using: transitionContext) | |
switch direction { | |
case .in: | |
to.view.alpha = 0 | |
UIView.animate(withDuration: 0.6, delay: 0.0, options: [.curveEaseOut], | |
animations: { [weak self] in | |
guard let self = self else { return } | |
self.to.view.alpha = 1 | |
}, completion: { _ in | |
transitionContext.completeTransition(true) | |
}) | |
case .out: | |
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.curveEaseIn], animations: { [weak self] in | |
guard let self = self else { return } | |
self.from.view.alpha = 0.0 | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
}) | |
} | |
} | |
} | |
/// Used for the always drop out animation with pan gesture dismissal | |
final internal class DismissInteractiveTransition: TransitionAnimator { | |
init() { | |
super.init(inDuration: 0.22, outDuration: 0.32, direction: .out) | |
} | |
override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
super.animateTransition(using: transitionContext) | |
UIView.animate(withDuration: outDuration, delay: 0.0, options: [.beginFromCurrentState], animations: { [weak self] in | |
guard let self = self else { return } | |
self.from.view.bounds.origin = CGPoint(x: 0, y: -self.from.view.bounds.size.height) | |
self.from.view.alpha = 0.0 | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment