281 lines
11 KiB
Swift
281 lines
11 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 unowned var containerView: UIView
|
|
private var interactivePopGestureRecognizerEnabled: Bool?
|
|
private var clipsToBounds: 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: CGRect
|
|
if #available(iOS 13.0, *) {
|
|
statusBarFrame = containerView.window?.windowScene?.statusBarManager?.statusBarFrame ?? .zero
|
|
} else {
|
|
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()
|
|
|
|
removeStyles(from: presentingViewController.containerViewController.view)
|
|
|
|
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
|
|
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
|
|
}
|
|
}
|