123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- import UIKit
- internal protocol PresentationModel {
-
- var statusBarEndAlpha: CGFloat { get }
-
- var presentingViewControllerUserInteractionEnabled: Bool { get }
-
- var presentingViewControllerUseSnapshot: Bool { get }
-
- var presentationStyle: SideMenuPresentationStyle { get }
-
- var menuWidth: CGFloat { get }
- }
- internal protocol SideMenuPresentationControllerDelegate: class {
- func sideMenuPresentationControllerDidTap(_ presentationController: SideMenuPresentationController)
- func sideMenuPresentationController(_ presentationController: SideMenuPresentationController, didPanWith gesture: UIPanGestureRecognizer)
- }
- internal final class SideMenuPresentationController {
- private let config: PresentationModel
- private weak var containerView: UIView?
- private var interactivePopGestureRecognizerEnabled: Bool?
- private var clipsToBounds: Bool?
- private let leftSide: Bool
- private weak var presentedViewController: UIViewController?
- private weak var presentingViewController: UIViewController?
- private lazy var snapshotView: UIView? = {
- guard config.presentingViewControllerUseSnapshot,
- let view = presentingViewController?.view.snapshotView(afterScreenUpdates: true) else {
- return nil
- }
- view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- return view
- }()
- private lazy var statusBarView: UIView? = {
- guard config.statusBarEndAlpha > .leastNonzeroMagnitude else { return nil }
- return UIView {
- $0.backgroundColor = config.presentationStyle.backgroundColor
- $0.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- $0.isUserInteractionEnabled = false
- }
- }()
- required init(config: PresentationModel, leftSide: Bool, presentedViewController: UIViewController, presentingViewController: UIViewController, containerView: UIView) {
- self.config = config
- self.containerView = containerView
- self.leftSide = leftSide
- self.presentedViewController = presentedViewController
- self.presentingViewController = presentingViewController
- }
- deinit {
- guard presentedViewController?.isHidden == false else { return }
-
- dismissalTransitionWillBegin()
- dismissalTransition()
- dismissalTransitionDidEnd(true)
- }
-
- func containerViewWillLayoutSubviews() {
- guard let containerView = containerView,
- let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- presentedViewController.view.untransform {
- presentedViewController.view.frame = frameOfPresentedViewInContainerView
- }
- presentingViewController.view.untransform {
- presentingViewController.view.frame = frameOfPresentingViewInContainerView
- snapshotView?.frame = presentingViewController.view.bounds
- }
- guard let statusBarView = statusBarView else { return }
- var statusBarFrame: CGRect = self.statusBarFrame
- statusBarFrame.size.height -= containerView.frame.minY
- statusBarView.frame = statusBarFrame
- }
-
- func presentationTransitionWillBegin() {
- guard let containerView = containerView,
- let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- if let snapshotView = snapshotView {
- presentingViewController.view.addSubview(snapshotView)
- }
- presentingViewController.view.isUserInteractionEnabled = config.presentingViewControllerUserInteractionEnabled
- containerView.backgroundColor = config.presentationStyle.backgroundColor
-
- layerViews()
- if let statusBarView = statusBarView {
- containerView.addSubview(statusBarView)
- }
-
- dismissalTransition()
- config.presentationStyle.presentationTransitionWillBegin(to: presentedViewController, from: presentingViewController)
- }
- func presentationTransition() {
- guard let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- transition(
- to: presentedViewController,
- from: presentingViewController,
- alpha: config.presentationStyle.presentingEndAlpha,
- statusBarAlpha: config.statusBarEndAlpha,
- scale: config.presentationStyle.presentingScaleFactor,
- translate: config.presentationStyle.presentingTranslateFactor
- )
- config.presentationStyle.presentationTransition(to: presentedViewController, from: presentingViewController)
- }
-
- func presentationTransitionDidEnd(_ completed: Bool) {
- guard completed else {
- snapshotView?.removeFromSuperview()
- dismissalTransitionDidEnd(!completed)
- return
- }
- guard let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- addParallax(to: presentingViewController.view)
-
- if let topNavigationController = presentingViewController as? UINavigationController {
- interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled ?? topNavigationController.interactivePopGestureRecognizer?.isEnabled
- topNavigationController.interactivePopGestureRecognizer?.isEnabled = false
- }
- containerViewWillLayoutSubviews()
- config.presentationStyle.presentationTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
- }
- func dismissalTransitionWillBegin() {
- snapshotView?.removeFromSuperview()
- presentationTransition()
- guard let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- config.presentationStyle.dismissalTransitionWillBegin(to: presentedViewController, from: presentingViewController)
- }
- func dismissalTransition() {
- guard let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- transition(
- to: presentingViewController,
- from: presentedViewController,
- alpha: config.presentationStyle.menuStartAlpha,
- statusBarAlpha: 0,
- scale: config.presentationStyle.menuScaleFactor,
- translate: config.presentationStyle.menuTranslateFactor
- )
- config.presentationStyle.dismissalTransition(to: presentedViewController, from: presentingViewController)
- }
- func dismissalTransitionDidEnd(_ completed: Bool) {
- guard completed else {
- if let snapshotView = snapshotView, let presentingViewController = presentingViewController {
- presentingViewController.view.addSubview(snapshotView)
- }
- presentationTransitionDidEnd(!completed)
- return
- }
- guard let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- statusBarView?.removeFromSuperview()
- removeStyles(from: presentingViewController.containerViewController.view)
-
- if let interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled,
- let topNavigationController = presentingViewController as? UINavigationController {
- topNavigationController.interactivePopGestureRecognizer?.isEnabled = interactivePopGestureRecognizerEnabled
- }
- presentingViewController.view.isUserInteractionEnabled = true
- config.presentationStyle.dismissalTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
- }
- }
- private extension SideMenuPresentationController {
- var statusBarFrame: CGRect {
- if #available(iOS 13.0, *) {
- return containerView?.window?.windowScene?.statusBarManager?.statusBarFrame ?? .zero
- } else {
- return UIApplication.shared.statusBarFrame
- }
- }
- var frameOfPresentedViewInContainerView: CGRect {
- guard let containerView = containerView else { return .zero }
- var rect = containerView.bounds
- rect.origin.x = leftSide ? 0 : rect.width - config.menuWidth
- rect.size.width = config.menuWidth
- return rect
- }
- var frameOfPresentingViewInContainerView: CGRect {
- guard let containerView = containerView else { return .zero }
- var rect = containerView.frame
- if containerView.superview != nil, containerView.frame.minY > .ulpOfOne {
- let statusBarOffset = statusBarFrame.height - rect.minY
- rect.origin.y = statusBarOffset
- rect.size.height -= statusBarOffset
- }
- return rect
- }
- func transition(to: UIViewController, from: UIViewController, alpha: CGFloat, statusBarAlpha: CGFloat, scale: CGFloat, translate: CGFloat) {
- containerViewWillLayoutSubviews()
-
- to.view.transform = .identity
- to.view.alpha = 1
- let x = (leftSide ? 1 : -1) * config.menuWidth * translate
- from.view.alpha = alpha
- from.view.transform = CGAffineTransform
- .identity
- .translatedBy(x: x, y: 0)
- .scaledBy(x: scale, y: scale)
- statusBarView?.alpha = statusBarAlpha
- }
- func layerViews() {
- guard let presentedViewController = presentedViewController,
- let presentingViewController = presentingViewController
- else { return }
- statusBarView?.layer.zPosition = 2
- if config.presentationStyle.menuOnTop {
- addShadow(to: presentedViewController.view)
- presentedViewController.view.layer.zPosition = 1
- } else {
- addShadow(to: presentingViewController.view)
- presentedViewController.view.layer.zPosition = -1
- }
- }
- func addShadow(to view: UIView) {
- view.layer.shadowColor = config.presentationStyle.onTopShadowColor.cgColor
- view.layer.shadowRadius = config.presentationStyle.onTopShadowRadius
- view.layer.shadowOpacity = config.presentationStyle.onTopShadowOpacity
- view.layer.shadowOffset = config.presentationStyle.onTopShadowOffset
- clipsToBounds = clipsToBounds ?? view.clipsToBounds
- view.clipsToBounds = false
- }
- func addParallax(to view: UIView) {
- var effects: [UIInterpolatingMotionEffect] = []
- let x = config.presentationStyle.presentingParallaxStrength.width
- if x > 0 {
- let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
- horizontal.minimumRelativeValue = -x
- horizontal.maximumRelativeValue = x
- effects.append(horizontal)
- }
- let y = config.presentationStyle.presentingParallaxStrength.height
- if y > 0 {
- let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
- vertical.minimumRelativeValue = -y
- vertical.maximumRelativeValue = y
- effects.append(vertical)
- }
- if effects.count > 0 {
- let group = UIMotionEffectGroup()
- group.motionEffects = effects
- view.motionEffects.removeAll()
- view.addMotionEffect(group)
- }
- }
- func removeStyles(from view: UIView) {
- view.motionEffects.removeAll()
- view.layer.shadowOpacity = 0
- view.layer.shadowOpacity = 0
- view.clipsToBounds = clipsToBounds ?? true
- clipsToBounds = false
- }
- }
|