Files
SideMenu/Pod/Classes/SideMenuPresentationController.swift

254 lines
9.7 KiB
Swift

//
// BasePresentationController.swift
// SideMenu
//
// Created by Jon Kent on 10/20/18.
//
import UIKit
internal protocol SideMenuPresentationControllerDelegate: class {
func sideMenuPresentationControllerDidTap(_ presentationController: SideMenuPresentationController)
func sideMenuPresentationController(_ presentationController: SideMenuPresentationController, didPanWith gesture: UIPanGestureRecognizer)
}
internal final class SideMenuPresentationController {
private let config: PresentationModel
private unowned var containerView: UIView
private var interactivePopGestureRecognizerEnabled: Bool?
private let leftSide: Bool
private weak var originalSuperview: UIView?
private unowned var presentedViewController: UIViewController
private unowned 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 != 0 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 else { return }
// Presentations must be reversed to preserve user experience
dismissalTransitionWillBegin()
dismissalTransition()
dismissalTransitionDidEnd(true)
}
var frameOfPresentedViewInContainerView: CGRect {
var rect = containerView.frame
rect.origin.x = leftSide ? 0 : rect.width - config.menuWidth
rect.size.width = config.menuWidth
return rect
}
func containerViewWillLayoutSubviews() {
presentedViewController.view.untransform {
presentedViewController.view.frame = frameOfPresentedViewInContainerView
}
presentingViewController.view.untransform {
presentingViewController.view.frame = containerView.frame
snapshotView?.frame = containerView.frame
}
guard let statusBarView = statusBarView else { return }
let statusBarOffset = containerView.frame.size.height - presentedViewController.view.bounds.height
var statusBarFrame = UIApplication.shared.statusBarFrame
// For in-call status bar, height is normally 40, which overlaps view. Instead, calculate height difference
// of view and set height to fill in remaining space.
if statusBarOffset >= CGFloat.ulpOfOne {
statusBarFrame.size.height = statusBarOffset
}
statusBarView.frame = statusBarFrame
}
func presentationTransitionWillBegin() {
if let snapshotView = snapshotView {
presentingViewController.view.addSubview(snapshotView)
}
presentingViewController.view.isUserInteractionEnabled = config.presentingViewControllerUserInteractionEnabled
containerView.backgroundColor = config.presentationStyle.backgroundColor
originalSuperview = presentingViewController.view.superview
containerView.addSubview(presentingViewController.view)
containerView.addSubview(presentedViewController.view)
layerViews()
if let statusBarView = statusBarView {
containerView.addSubview(statusBarView)
}
dismissalTransition()
config.presentationStyle.presentationTransitionWillBegin(to: presentedViewController, from: presentingViewController)
}
func presentationTransition() {
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
}
addParallax(to: presentingViewController.view)
if let topNavigationController = presentingViewController as? UINavigationController {
interactivePopGestureRecognizerEnabled = topNavigationController.interactivePopGestureRecognizer?.isEnabled
topNavigationController.interactivePopGestureRecognizer?.isEnabled = false
}
containerViewWillLayoutSubviews()
config.presentationStyle.presentationTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
}
func dismissalTransitionWillBegin() {
snapshotView?.removeFromSuperview()
presentationTransition()
config.presentationStyle.dismissalTransitionWillBegin(to: presentedViewController, from: presentingViewController)
}
func dismissalTransition() {
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 {
presentingViewController.view.addSubview(snapshotView)
}
presentationTransitionDidEnd(!completed)
return
}
statusBarView?.removeFromSuperview()
presentedViewController.view.removeFromSuperview()
presentingViewController.view.motionEffects.removeAll()
presentingViewController.view.layer.shadowOpacity = 0
presentedViewController.view.layer.shadowOpacity = 0
if let interactivePopGestureRecognizerEnabled = interactivePopGestureRecognizerEnabled,
let topNavigationController = presentingViewController as? UINavigationController {
topNavigationController.interactivePopGestureRecognizer?.isEnabled = interactivePopGestureRecognizerEnabled
}
originalSuperview?.addSubview(presentingViewController.view)
presentingViewController.view.isUserInteractionEnabled = true
config.presentationStyle.dismissalTransitionDidEnd(to: presentedViewController, from: presentingViewController, completed)
}
}
private extension SideMenuPresentationController {
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() {
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
}
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)
}
}
}