Files
SideMenu/Pod/Classes/SideMenuPresentationController.swift

317 lines
13 KiB
Swift

//
// BasePresentationController.swift
// SideMenu
//
// Created by Jon Kent on 10/20/18.
//
import UIKit
internal protocol PresentationModel {
/// Draws `presentStyle.backgroundColor` behind the status bar. Default is 1.
var statusBarEndAlpha: CGFloat { get }
/// Enable or disable interaction with the presenting view controller while the menu is displayed. Enabling may make it difficult to dismiss the menu or cause exceptions if the user tries to present and already presented menu. `presentingViewControllerUseSnapshot` must also set to false. Default is false.
var presentingViewControllerUserInteractionEnabled: Bool { get }
/// Use a snapshot for the presenting vierw controller while the menu is displayed. Useful when layout changes occur during transitions. Not recommended for apps that support rotation. Default is false.
var presentingViewControllerUseSnapshot: Bool { get }
/// The presentation style of the menu.
var presentationStyle: SideMenuPresentationStyle { get }
/// Width of the menu when presented on screen, showing the existing view controller in the remaining space. Default is zero.
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 }
// Presentations must be reversed to preserve user experience
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
}
}