123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- //
- // PresentrPresentationController.swift
- // OneUP
- //
- // Created by Daniel Lozano on 4/27/16.
- // Copyright © 2016 Icalia Labs. All rights reserved.
- //
- import UIKit
- /// Presentr's custom presentation controller. Handles the position and sizing for the view controller's.
- class PresentrController: UIPresentationController, UIAdaptivePresentationControllerDelegate {
-
- /// Presentation type must be passed in to make all the sizing and position decisions.
- let presentationType: PresentationType
- /// Should the presented controller dismiss on background tap.
- let dismissOnTap: Bool
-
- /// Should the presented controller dismiss on background Swipe.
- let dismissOnSwipe: Bool
- /// DismissSwipe direction
- let dismissOnSwipeDirection: DismissSwipeDirection
-
- /// Should the presented controller use animation when dismiss on background tap.
- let dismissAnimated: Bool
- /// How the presented view controller should respond in response to keyboard presentation.
- let keyboardTranslationType: KeyboardTranslationType
- /// The frame used for a current context presentation. If nil, normal presentation.
- let contextFrameForPresentation: CGRect?
- /// If contextFrameForPresentation is set, this handles what happens when tap outside context frame.
- let shouldIgnoreTapOutsideContext: Bool
- /// A custom background view to be added on top of the regular background view.
- let customBackgroundView: UIView?
- fileprivate var conformingPresentedController: PresentrDelegate? {
- return presentedViewController as? PresentrDelegate
- }
- fileprivate var shouldObserveKeyboard: Bool {
- return conformingPresentedController != nil ||
- (keyboardTranslationType != .none && presentationType == .popup) // TODO: Work w/other types?
- }
- fileprivate var containerFrame: CGRect {
- return contextFrameForPresentation ?? containerView?.bounds ?? CGRect()
- }
- fileprivate var keyboardIsShowing: Bool = false
- // MARK: Background Views
- fileprivate var chromeView = UIView()
- fileprivate var backgroundView = PassthroughBackgroundView()
- fileprivate var visualEffect: UIVisualEffect?
- // MARK: Swipe gesture
- fileprivate var presentedViewIsBeingDissmissed: Bool = false
- fileprivate var presentedViewFrame: CGRect = .zero
- fileprivate var presentedViewCenter: CGPoint = .zero
- fileprivate var latestShouldDismiss: Bool = true
- fileprivate lazy var shouldSwipeBottom: Bool = {
- return self.dismissOnSwipeDirection == .default ? self.presentationType != .topHalf : self.dismissOnSwipeDirection == .bottom
- }()
- fileprivate lazy var shouldSwipeTop: Bool = {
- return self.dismissOnSwipeDirection == .default ? self.presentationType == .topHalf : self.dismissOnSwipeDirection == .top
- }()
- // MARK: - Init
- init(presentedViewController: UIViewController,
- presentingViewController: UIViewController?,
- presentationType: PresentationType,
- roundCorners: Bool?,
- cornerRadius: CGFloat,
- dropShadow: PresentrShadow?,
- dismissOnTap: Bool,
- dismissOnSwipe: Bool,
- dismissOnSwipeDirection: DismissSwipeDirection,
- backgroundColor: UIColor,
- backgroundOpacity: Float,
- blurBackground: Bool,
- blurStyle: UIBlurEffect.Style,
- customBackgroundView: UIView?,
- keyboardTranslationType: KeyboardTranslationType,
- dismissAnimated: Bool,
- contextFrameForPresentation: CGRect?,
- shouldIgnoreTapOutsideContext: Bool) {
- self.presentationType = presentationType
- self.dismissOnTap = dismissOnTap
- self.dismissOnSwipe = dismissOnSwipe
- self.dismissOnSwipeDirection = dismissOnSwipeDirection
- self.keyboardTranslationType = keyboardTranslationType
- self.dismissAnimated = dismissAnimated
- self.contextFrameForPresentation = contextFrameForPresentation
- self.shouldIgnoreTapOutsideContext = shouldIgnoreTapOutsideContext
- self.customBackgroundView = customBackgroundView
- super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
-
- setupBackground(backgroundColor, backgroundOpacity: backgroundOpacity, blurBackground: blurBackground, blurStyle: blurStyle)
- setupCornerRadius(roundCorners: roundCorners, cornerRadius: cornerRadius)
- addDropShadow(shadow: dropShadow)
-
- if dismissOnSwipe {
- setupDismissOnSwipe()
- }
- if shouldObserveKeyboard {
- registerKeyboardObserver()
- }
- }
- // MARK: - Setup
- private func setupDismissOnSwipe() {
- let swipe = UIPanGestureRecognizer(target: self, action: #selector(presentedViewSwipe))
- presentedViewController.view.addGestureRecognizer(swipe)
- }
-
- private func setupBackground(_ backgroundColor: UIColor, backgroundOpacity: Float, blurBackground: Bool, blurStyle: UIBlurEffect.Style) {
- let tap = UITapGestureRecognizer(target: self, action: #selector(chromeViewTapped))
- chromeView.addGestureRecognizer(tap)
- if !shouldIgnoreTapOutsideContext {
- let tap = UITapGestureRecognizer(target: self, action: #selector(chromeViewTapped))
- backgroundView.addGestureRecognizer(tap)
- }
- if blurBackground {
- visualEffect = UIBlurEffect(style: blurStyle)
- } else {
- chromeView.backgroundColor = backgroundColor.withAlphaComponent(CGFloat(backgroundOpacity))
- }
- }
- private func setupCornerRadius(roundCorners: Bool?, cornerRadius: CGFloat) {
- let shouldRoundCorners = roundCorners ?? presentationType.shouldRoundCorners
- if shouldRoundCorners {
- presentedViewController.view.layer.cornerRadius = cornerRadius
- presentedViewController.view.layer.masksToBounds = true
- } else {
- presentedViewController.view.layer.cornerRadius = 0
- }
- }
-
- private func addDropShadow(shadow: PresentrShadow?) {
- guard let shadow = shadow else {
- presentedViewController.view.layer.masksToBounds = true
- presentedViewController.view.layer.shadowOpacity = 0
- return
- }
- presentedViewController.view.layer.masksToBounds = false
- if let shadowColor = shadow.shadowColor?.cgColor {
- presentedViewController.view.layer.shadowColor = shadowColor
- }
- if let shadowOpacity = shadow.shadowOpacity {
- presentedViewController.view.layer.shadowOpacity = shadowOpacity
- }
- if let shadowOffset = shadow.shadowOffset {
- presentedViewController.view.layer.shadowOffset = shadowOffset
- }
- if let shadowRadius = shadow.shadowRadius {
- presentedViewController.view.layer.shadowRadius = shadowRadius
- }
- }
-
- fileprivate func registerKeyboardObserver() {
- NotificationCenter.default.addObserver(self, selector: #selector(PresentrController.keyboardWasShown(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(PresentrController.keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
- }
-
- fileprivate func removeObservers() {
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
- NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
- }
- }
- // MARK: - UIPresentationController
- extension PresentrController {
-
- // MARK: Presentation
-
- override var frameOfPresentedViewInContainerView: CGRect {
- var presentedViewFrame = CGRect.zero
- let containerBounds = containerFrame
- let size = self.size(forChildContentContainer: presentedViewController, withParentContainerSize: containerBounds.size)
-
- let origin: CGPoint
- // If the Presentation Type's calculate center point returns nil
- // this means that the user provided the origin, not a center point.
- if let center = getCenterPointFromType() {
- origin = calculateOrigin(center, size: size)
- } else {
- origin = getOriginFromType() ?? CGPoint(x: 0, y: 0)
- }
-
- presentedViewFrame.size = size
- presentedViewFrame.origin = origin
- return presentedViewFrame
- }
-
- override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize {
- let width = getWidthFromType(parentSize)
- let height = getHeightFromType(parentSize)
- return CGSize(width: CGFloat(width), height: CGFloat(height))
- }
-
- override func containerViewWillLayoutSubviews() {
- guard !keyboardIsShowing else {
- return // prevent resetting of presented frame when the frame is being translated
- }
- chromeView.frame = containerFrame
- presentedView!.frame = frameOfPresentedViewInContainerView
- }
-
- // MARK: Animation
-
- override func presentationTransitionWillBegin() {
- guard let containerView = containerView else {
- return
- }
- setupBackgroundView()
- backgroundView.frame = containerView.bounds
- chromeView.frame = containerFrame
- containerView.insertSubview(backgroundView, at: 0)
- containerView.insertSubview(chromeView, at: 1)
- if let customBackgroundView = customBackgroundView {
- chromeView.addSubview(customBackgroundView)
- }
- var blurEffectView: UIVisualEffectView?
- if visualEffect != nil {
- let view = UIVisualEffectView()
- view.frame = chromeView.bounds
- view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- chromeView.insertSubview(view, at: 0)
- blurEffectView = view
- } else {
- chromeView.alpha = 0.0
- }
- guard let coordinator = presentedViewController.transitionCoordinator else {
- chromeView.alpha = 1.0
- return
- }
- coordinator.animate(alongsideTransition: { context in
- blurEffectView?.effect = self.visualEffect
- self.chromeView.alpha = 1.0
- }, completion: nil)
- }
-
- override func dismissalTransitionWillBegin() {
- guard let coordinator = presentedViewController.transitionCoordinator else {
- chromeView.alpha = 0.0
- return
- }
- coordinator.animate(alongsideTransition: { context in
- self.chromeView.alpha = 0.0
- }, completion: nil)
- }
- // MARK: - Animation Helper's
- func setupBackgroundView() {
- if shouldIgnoreTapOutsideContext {
- backgroundView.shouldPassthrough = true
- backgroundView.passthroughViews = presentingViewController.view.subviews
- } else {
- backgroundView.shouldPassthrough = false
- backgroundView.passthroughViews = []
- }
- }
- }
- // MARK: - Sizing, Position
- fileprivate extension PresentrController {
- func getWidthFromType(_ parentSize: CGSize) -> Float {
- guard let size = presentationType.size() else {
- if case .dynamic = presentationType {
- return Float(presentedViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).width)
- }
- return 0
- }
- return size.width.calculateWidth(parentSize)
- }
- func getHeightFromType(_ parentSize: CGSize) -> Float {
- guard let size = presentationType.size() else {
- if case .dynamic = presentationType {
- return Float(presentedViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height)
- }
- return 0
- }
- return size.height.calculateHeight(parentSize)
- }
- func getCenterPointFromType() -> CGPoint? {
- let containerBounds = containerFrame
- let position = presentationType.position()
- return position.calculateCenterPoint(containerBounds)
- }
- func getOriginFromType() -> CGPoint? {
- let position = presentationType.position()
- return position.calculateOrigin()
- }
- func calculateOrigin(_ center: CGPoint, size: CGSize) -> CGPoint {
- let x: CGFloat = center.x - size.width / 2
- let y: CGFloat = center.y - size.height / 2
- return CGPoint(x: x, y: y)
- }
-
- }
- // MARK: - Gesture Handling
- extension PresentrController {
- @objc func chromeViewTapped(gesture: UIGestureRecognizer) {
- guard dismissOnTap else {
- return
- }
- guard conformingPresentedController?.presentrShouldDismiss?(keyboardShowing: keyboardIsShowing) ?? true else {
- return
- }
- if gesture.state == .ended {
- if shouldObserveKeyboard {
- removeObservers()
- }
- presentingViewController.dismiss(animated: dismissAnimated, completion: nil)
- }
- }
- @objc func presentedViewSwipe(gesture: UIPanGestureRecognizer) {
- guard dismissOnSwipe else {
- return
- }
- if gesture.state == .began {
- presentedViewFrame = presentedViewController.view.frame
- presentedViewCenter = presentedViewController.view.center
- let directionDown = gesture.translation(in: presentedViewController.view).y > 0
- if (shouldSwipeBottom && directionDown) || (shouldSwipeTop && !directionDown) {
- latestShouldDismiss = conformingPresentedController?.presentrShouldDismiss?(keyboardShowing: keyboardIsShowing) ?? true
- }
- } else if gesture.state == .changed {
- swipeGestureChanged(gesture: gesture)
- } else if gesture.state == .ended || gesture.state == .cancelled {
- swipeGestureEnded()
- }
- }
- // MARK: Helper's
- func swipeGestureChanged(gesture: UIPanGestureRecognizer) {
- let amount = gesture.translation(in: presentedViewController.view)
- if shouldSwipeTop && amount.y > 0 {
- return
- } else if shouldSwipeBottom && amount.y < 0 {
- return
- }
- var swipeLimit: CGFloat = 100
- if shouldSwipeTop {
- swipeLimit = -swipeLimit
- }
- presentedViewController.view.center = CGPoint(x: presentedViewCenter.x, y: presentedViewCenter.y + amount.y)
- let dismiss = shouldSwipeTop ? (amount.y < swipeLimit) : ( amount.y > swipeLimit)
- if dismiss && latestShouldDismiss {
- presentedViewIsBeingDissmissed = true
- presentedViewController.dismiss(animated: dismissAnimated, completion: nil)
- }
- }
- func swipeGestureEnded() {
- guard !presentedViewIsBeingDissmissed else {
- return
- }
- UIView.animate(withDuration: 0.5,
- delay: 0,
- usingSpringWithDamping: 0.5,
- initialSpringVelocity: 1,
- options: [],
- animations: {
- self.presentedViewController.view.frame = self.presentedViewFrame
- }, completion: nil)
- }
- }
- // MARK: - Keyboard Handling
- extension PresentrController {
- @objc func keyboardWasShown(notification: Notification) {
- if let keyboardFrame = notification.keyboardEndFrame() {
- let presentedFrame = frameOfPresentedViewInContainerView
- let translatedFrame = keyboardTranslationType.getTranslationFrame(keyboardFrame: keyboardFrame, presentedFrame: presentedFrame)
- if translatedFrame != presentedFrame {
- UIView.animate(withDuration: notification.keyboardAnimationDuration() ?? 0.5, animations: {
- self.presentedView?.frame = translatedFrame
- })
- }
- keyboardIsShowing = true
- }
- }
- @objc func keyboardWillHide (notification: Notification) {
- if keyboardIsShowing {
- let presentedFrame = frameOfPresentedViewInContainerView
- if self.presentedView?.frame != presentedFrame {
- UIView.animate(withDuration: notification.keyboardAnimationDuration() ?? 0.5, animations: {
- self.presentedView?.frame = presentedFrame
- })
- }
- keyboardIsShowing = false
- }
- }
- }
|