Pass UILongPressGestureRecognizer between UIViewControllers - ios

I've looked around on SO and other places online and haven't been able to find a solution yet. I have a UILongPressGestureRecognizer on one view controller which performs an animation on a UIView then presents another view controller (if the user does not move their finger during the animation). I would like to dismiss the second view controller when the user lifts their finger, but I am having trouble.
My current approach is adding another UILongPressGestureRecognizer to the second view controller and dismissing the view controller when its state is .ended; however I don't know how to tie the two gesture recognizers together so that I can start the touch in one view controller and end in another.
I have attached the relevant code below
In the first View Controller
#objc private func pinAnimation(sender: UILongPressGestureRecognizer) {
if let headerView = projectView.projectCollectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(row: 0, section: 0)) as? ProjectHeaderView {
switch sender.state {
case .began:
let frame = headerView.pinButton.frame
UIView.setAnimationCurve(.linear)
UIView.animate(withDuration: 1, animations: {
headerView.pinProgressView.frame = frame
}, completion: { (_) in
if (headerView.pinProgressView.frame == headerView.pinButton.frame) {
let notification = UINotificationFeedbackGenerator()
notification.notificationOccurred(.success)
let homeVC = HomeViewController()
self.present(homeVC, animated: true, completion: {
homeVC.longPress(sender)
let height = headerView.pinButton.frame.height
let minX = headerView.pinButton.frame.minX
let minY = headerView.pinButton.frame.minY
headerView.pinProgressView.frame = CGRect(x: minX, y: minY, width: 0, height: height)
})
}
})
case .ended:
//cancel current animation
let height = headerView.pinProgressView.frame.height
let minX = headerView.pinProgressView.frame.minX
let minY = headerView.pinProgressView.frame.minY
UIView.setAnimationCurve(.linear)
UIView.animate(withDuration: 0.5) {
headerView.pinProgressView.frame = CGRect(x: minX, y: minY, width: 0, height: height)
}
default:
break
}
}
}
In the second View Controller
override func viewDidLoad() {
super.viewDidLoad()
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
view.addGestureRecognizer(longPressRecognizer)
}
#objc func longPress(_ sender: UILongPressGestureRecognizer) {
switch sender.state {
case .ended:
self.dismiss(animated: true, completion: nil)
default:
break
}
}
Any help / guidance will be much appreciated!

I would recommend you using delegate pattern:
Create delegate for your first ViewController
protocol FirstViewControllerDelegate {
func longPressEnded()
}
then add delegate variable to your first ViewController
class FirstViewController {
var delegate: FirstViewControllerDelegate?
...
}
next call method on delegate when long press ended
#objc private func pinAnimation(sender: UILongPressGestureRecognizer) {
...
switch sender.state {
case .began:
...
case .ended:
...
delegate?.longPressEnded()
default:
break
}
}
after that when you're presenting second ViewController, set first ViewController's delegate as homeVC
let homeVC = HomeViewController()
self.delegate = homeVC
then implement this delegate to second ViewController and define what should happen when long press ended
class HomeViewController: UIViewController, FirstViewControllerDelegate {
...
func longPressEnded() {
dismiss(animated: true, completion: nil)
}
}

Related

Is there a way to have interactive modal dismissal for a .fullscreen modally presented view controller?

I want to enable interactive modal dismissal that pans along with a users finger on a fullscreen modally presented view controller .fullscreen.
I've seen that it's fairly trivial to do so on the .pageSheet and the .formSheet which have it built in but have not seen a clear example for the full screen.
I'm guessing I'd need to have a pan gesture added to my vc within the body of it's code and then adjust for the states myself but wondering if anyone knows what exactly needs to be done / if there's a simpler way to do it as it seems much more complicated for the .fullscreen case
It can be done with creating your custom UIPresentationController and UIViewControllerTransitioningDelegate. Lets say we have TestViewController and we want to present SecondViewController with total presentedHeight of 1.0 (fullScreen). Presentation will be triggered with #IBAction func buttonPressed and can be dismissed by dragging controller down (as we are used to it). It would be also nice to add some backgroundEffect to be gradually changed while user is sliding down the SecondViewController (especially when used only presentedHeight of 0.6).
Firstly we define OverlayViewController which will be later superclass of presented SecondViewControllerand will contain UIPanGestureRecognizer.
class OverlayViewController: UIViewController {
var hasSetPointOrigin = false
var pointOrigin: CGPoint?
var delegate: OverlayViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction))
view.addGestureRecognizer(panGesture)
}
override func viewDidLayoutSubviews() {
if !hasSetPointOrigin {
hasSetPointOrigin = true
pointOrigin = self.view.frame.origin
}
}
#objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
// Not allowing the user to drag the view upward
guard translation.y >= 0 else { return }
let currentPosition = translation.y
let originPos = self.pointOrigin
delegate?.userDragged(draggedPercentage: translation.y/originPos!.y)
// setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down
view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
if sender.state == .ended {
let dragVelocity = sender.velocity(in: view)
if dragVelocity.y >= 1100 {
self.dismiss(animated: true, completion: nil)
} else {
// Set back to original position of the view controller
UIView.animate(withDuration: 0.3) {
self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
self.delegate?.animateBlurBack(seconds: 0.3)
}
}
}
}
}
protocol OverlayViewDelegate: AnyObject {
func userDragged(draggedPercentage: CGFloat)
func animateBlurBack(seconds: TimeInterval)
}
Next we define custom PresentationController
class PresentationController: UIPresentationController {
private var backgroundEffectView: UIView?
private var backgroundEffect: BackgroundEffect?
private var viewHeight: CGFloat?
private let maxDim:CGFloat = 0.6
private var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
convenience init(presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?,
backgroundEffect: BackgroundEffect = .blur,
viewHeight: CGFloat = 0.6)
{
self.init(presentedViewController: presentedViewController, presenting: presentingViewController)
self.backgroundEffect = backgroundEffect
self.backgroundEffectView = returnCorrectEffectView(backgroundEffect)
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
self.backgroundEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.backgroundEffectView?.isUserInteractionEnabled = true
self.backgroundEffectView?.addGestureRecognizer(tapGestureRecognizer)
self.viewHeight = viewHeight
}
private override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * (1-viewHeight!)),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
viewHeight!))
}
override func presentationTransitionWillBegin() {
self.backgroundEffectView?.alpha = 0
self.containerView?.addSubview(backgroundEffectView!)
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}, completion: { (UIViewControllerTransitionCoordinatorContext) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.alpha = 0
}, completion: { (UIViewControllerTransitionCoordinatorContext) in
self.backgroundEffectView?.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
backgroundEffectView?.frame = containerView!.bounds
}
#objc func dismissController(){
self.presentedViewController.dismiss(animated: true, completion: nil)
}
func graduallyChangeOpacity(withPercentage: CGFloat) {
self.backgroundEffectView?.alpha = withPercentage
}
func returnCorrectEffectView(_ effect: BackgroundEffect) -> UIView {
switch effect {
case .blur:
var blurEffect = UIBlurEffect(style: .dark)
if self.traitCollection.userInterfaceStyle == .dark {
blurEffect = UIBlurEffect(style: .light)
}
return UIVisualEffectView(effect: blurEffect)
case .dim:
var dimView = UIView()
dimView.backgroundColor = .black
if self.traitCollection.userInterfaceStyle == .dark {
dimView.backgroundColor = .gray
}
dimView.alpha = maxDim
return dimView
case .none:
let clearView = UIView()
clearView.backgroundColor = .clear
return clearView
}
}
}
extension PresentationController: OverlayViewDelegate {
func userDragged(draggedPercentage: CGFloat) {
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
switch self.backgroundEffect! {
case .blur:
graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
case .dim:
graduallyChangeOpacity(withPercentage: maxDim-draggedPercentage)
case .none:
self.backgroundEffectView?.alpha = 0
}
}
func animateBlurBack(seconds: TimeInterval) {
UIView.animate(withDuration: seconds) {
switch self.backgroundEffect! {
case .blur:
self.backgroundEffectView?.alpha = 1
case .dim:
self.backgroundEffectView?.alpha = self.maxDim
case .none:
self.backgroundEffectView?.alpha = 0
}
}
}
}
enum BackgroundEffect {
case blur
case dim
case none
}
Create SecondViewController subclassing OverlayViewController:
class SecondViewController: OverlayViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
addSlider()
}
func addSlider() {
let sliderWidth:CGFloat = 100
let centerOfScreen = self.view.frame.size.width / 2
let rect = CGRect(x: centerOfScreen - sliderWidth/2, y: 80, width: sliderWidth, height: 10)
let slider = UIView(frame: rect)
slider.backgroundColor = .black
self.view.addSubview(slider)
}
Add showOverlay() function that will be triggered after buttonPressed and conform your presenting UIViewController (TestViewController) to UIViewControllerTransitioningDelegate :
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func buttonPressed(_ sender: Any) {
showOverlay()
}
func showOverlay() {
let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondVC") as! SecondViewController
secondVC.modalPresentationStyle = .custom
secondVC.transitioningDelegate = self
self.present(secondVC, animated: true, completion: nil)
}
}
extension TestViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController?
{
let presentedHeight: CGFloat = 1.0
let controller = PresentationController(presentedViewController: presented,
presenting: presenting,
backgroundEffect: .dim,
viewHeight: presentedHeight)
if let vc = presented as? OverlayViewController {
vc.delegate = controller
}
return controller
}
}
Now we should be able to present SecondViewController with showOverlay() function setting its presentedHeight to 1.0 and .dim background effect. We can dismiss SecondViewController similar to another modal presentations.

Interacting with a ViewController that has Multiple child ViewControllers

I have a parent viewController and a child viewController. The childViewController is like a card and functions similar to Apple's stock or map app. I can expand or collapse it and interact with the buttons within it. The only problem is that I can't interact with any buttons within the parent viewController. Anyone know what's the problem.
class BaseViewController: UIViewController {
enum CardState {
case expanded
case collapsed
}
var cardViewController: CardViewController!
var visualEffectView: UIVisualEffectView!
lazy var deviceCardHeight: CGFloat = view.frame.height - (view.frame.height / 6)
lazy var cardHeight: CGFloat = deviceCardHeight
let cardHandleAreaHeight: CGFloat = 65
var cardVisible = false
var nextState: CardState {
return cardVisible ? .collapsed : .expanded
}
override func viewDidLoad() {
super.viewDidLoad()
setupCard()
}
#IBAction func expandCard(_ sender: Any) {
print("Button Pressed")
}
func setupCard() {
cardViewController = CardViewController(nibName: "CardViewController", bundle: nil)
self.addChild(cardViewController)
self.view.addSubview(cardViewController.view)
//Set up frame of cardView
cardViewController.view.frame = CGRect(x: 0, y: view.frame.height - cardHandleAreaHeight, width: view.frame.width, height: cardHeight)
cardViewController.view.clipsToBounds = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCardTap(recognizer:)))
cardViewController.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleCardTap(recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
animationTransitionIfNeeded(state: nextState, duration: 0.9)
default:
break
}
}
func animationTransitionIfNeeded(state: CardState, duration: TimeInterval) {
if runningAnimations.isEmpty {
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
switch state {
case .expanded:
self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardHeight
case .collapsed:
self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardHandleAreaHeight
}
}
frameAnimator.addCompletion { _ in
//if true set to false, if false set to true
self.cardVisible = !self.cardVisible
self.runningAnimations.removeAll()
}
frameAnimator.startAnimation()
runningAnimations.append(frameAnimator)
let cornerRadiusAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
switch state {
case .expanded:
self.cardViewController.view.layer.cornerRadius = 12
case .collapsed:
self.cardViewController.view.layer.cornerRadius = 0
}
}
cornerRadiusAnimator.startAnimation()
}
}
'Can't interact' should mean that you can't press. If that is the case the most likely cause is that the button is covered with something (it could be transparent so ensure when testing that there is no transparent backgrounds until you resolve this). The other possible reason would be that you have set some property of the button that would cause this behavior, but you would probably know that so it is almost certainly the former.
I solve it. This code should work perfectly. The reason why it didn't before was because I added in a visualEffectView. It was covering the parent.

How do you really hide and show a tab bar when tapping on part of your view? (no buttons but any location of the screen)

override func viewDidLoad() {
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
tabBarController?.hideTabBarAnimated(hide: true)
}
extension UITabBarController {
func hideTabBarAnimated(hide:Bool) {
UIView.animate(withDuration: 2, animations: {
if hide {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: 100)
} else {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: -100)
}
})
}
}
I can only hide the tab bar but I can't make it show when you tap again. I tried to look for answers on stack overflow but the answers seems to only work if you're using a button or a storyboard.
Have a variable isTabBarHidden in class which stores if the tabBar has been animated to hide. (You could have used tabBar.isHidden, but that would complicate the logic a little bit when animate hiding and showing)
class ViewController {
var isTabBarHidden = false // set the default value as required
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
guard let tabBarControllerFound = tabBarController else {
return
}
tabBarController?.hideTabBarAnimated(hide: !isTabBarHidden)
isTabBarHidden = !isTabBarHidden
}
}
Generalised solution with protocol which will work in all the screens
Create UIViewController named BaseViewController and make it base class of all of your view controllers
Now Define protocol
protocol ProtocolHideTabbar:class {
func hideTabbar ()
}
protocol ProtocolShowTabbar:class {
func showTabbar ()
}
extension ProtocolHideTabbar where Self : UIViewController {
func hideTabbar () {
self.tabBarController?.tabBar.isHidden = true
}
}
extension ProtocolShowTabbar where Self : UIViewController {
func showTabbar () {
self.tabBarController?.tabBar.isHidden = false
}
}
By default we want show tabbar in every view controller so
extension UIViewController : ProtocolShowTabbar {}
In your BaseView Controller
in view will appear method add following code to show hide based on protocol
if self is ProtocolHideTabbar {
( self as! ProtocolHideTabbar).hideTabbar()
} else if self is ProtocolShowTabbar{
( self as ProtocolShowTabbar).showTabbar()
}
How to use
Simply
class YourViewControllerWithTabBarHidden:BaseViewController,ProtocolHideTabbar {
}
Hope it is helpful
Tested 100% working
Please try below code for that in UITabBarController subclass
var isTabBarHidden:Bool = false
func setTabBarHidden(_ tabBarHidden: Bool, animated: Bool,completion:((Void) -> Void)? = nil) {
if tabBarHidden == isTabBarHidden {
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
//check tab bar is visible and view and window height is same then it should be 49 + window Heigth
if (tabBarHidden == true && UIScreen.main.bounds.height == self.view.frame.height) {
let offset = self.tabBar.frame.size.height
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset)
}
if let block = completion {
block()
}
return
}
let offset: CGFloat? = tabBarHidden ? self.tabBar.frame.size.height : -self.tabBar.frame.size.height
UIView.animate(withDuration: animated ? 0.250 : 0.0, delay: 0.1, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [.curveEaseIn, .layoutSubviews], animations: {() -> Void in
self.tabBar.center = CGPoint(x: CGFloat(self.tabBar.center.x), y: CGFloat(self.tabBar.center.y + offset!))
//Check if View is already at bottom so we don't want to move view more up (it will show black screen on bottom ) Scnario : When present mail app
if (Int(offset!) <= 0 && UIScreen.main.bounds.height == self.view.frame.height) == false {
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset!)
}
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}, completion: { _ in
if let block = completion {
block()
}
})
isTabBarHidden = tabBarHidden
}
Hope it is helpful

Interactive sliding menu goes to completion as soon as pan gesture starts [Swift]

I am trying to implement a sliding menu that can be interactively dismissed by horizontal panning, same as the ones in Uber and Google apps. Everything works as expected except that, as soon as I start panning horizontally, dismiss goes to completion without following my finger. Any suggestion of where the problem may lie is very appreciated.
I subclassed UIPresentationController to define the presented width of my menu controller. I have custom presentation animator and dismiss animator, and a UIViewControllerTransitioningDelegate object to return them all to UIKit. I also implemented gestureRecognizerShouldBegin(_ gestureRecognizer:) method in my menu controller to allow vertical scrolling.
SlideDismissAnimator
class SlideDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let interactionController: SlideInteractionController?
init(interactionController: SlideInteractionController?) {
self.interactionController = interactionController
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let fromCV = transitionContext.viewController(forKey: .from)!
let initialFrame = transitionContext.finalFrame(for: fromCV)
var finalFrame = initialFrame
finalFrame.origin.x = transitionContext.containerView.frame.width // My menu slides in from right
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
fromCV.view.frame = finalFrame
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
SlideInteractionController
class SlideInteractionController: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
private var shouldCompleteTransition = false
private weak var collectionViewController: UICollectionViewController!
init(collectionViewController: UICollectionViewController) {
super.init()
self.collectionViewController = collectionViewController
if let menuController = collectionViewController as? MenuController {
let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
menuController.collectionView?.addGestureRecognizer(gesture)
gesture.delegate = menuController
}
}
#objc func handleGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
var progress = (translation.x / 100)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
collectionViewController.dismiss(animated: true, completion: nil)
case .changed:
shouldCompleteTransition = progress > 0.5
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
}
MenuController
class MenuController: UICollectionViewController, UIGestureRecognizerDelegate {
var slideInteractionController: SlideInteractionController?
override func viewDidLoad() {
super.viewDidLoad()
setupView()
slideInteractionController = SlideInteractionController(collectionViewController: self)
}
...
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let translation = panGestureRecognizer.translation(in: collectionView)
if translation.x > fabs(translation.y) {
return true
}
}
return false
}
}
I made a sample Project that Use a tableView inside a View that acts as A side Menu drawer Presented over Current Context
my pan Handler
//MARK: Pan gesture Handler
#objc func handlePanGesture(panGesture: UIPanGestureRecognizer)
{
///Get the changes
let translation = panGesture.translation(in: self.view)
///Make View move to left side of Frame
if CGFloat(round(Double((panGesture.view?.frame.origin.x)!))) <= 0
{
panGesture.view!.center = CGPoint(x: panGesture.view!.center.x + translation.x, y: panGesture.view!.center.y)
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
///Do not let View go beyond origin as 0
if CGFloat(round(Double((panGesture.view?.frame.origin.x)!))) > 0
{
panGesture.view?.frame.origin.x = 0
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
///States When Dragging
switch panGesture.state
{
case .changed:
self.setAlphaOfBlurView(origin: (panGesture.view?.frame.maxX)!)
case .ended:
if CGFloat(round(Double((panGesture.view?.frame.maxX)!))) >= self.view.frame.size.width*0.35
{
UIView.animate(withDuration: 0.7, animations: {
panGesture.view?.frame.origin.x = 0
panGesture.setTranslation(CGPoint.zero, in: self.view)
})
}
else
{
UIView.animate(withDuration: 0.4, animations: {
panGesture.view?.frame.origin.x -= self.maximum_x
panGesture.setTranslation(CGPoint.zero, in: self.view)
}, completion: { (success) in
if (success)
{
self.remove(asChildViewController: self.sideMenuVCObject, baseView: self.baseView)
self.baseView.removeFromSuperview()
self.blurView.removeFromSuperview()
//Remove Notification observer
NotificationCenter.default.removeObserver(self,name: NSNotification.Name(rawValue: "hideMenu"),object: nil)
}
})
}
break
default:
print("Default Case")
}
}
Repository Link at GiHub
https://github.com/RockinGarg/Slide-Menu-Drawer.git
Working Video :
https://drive.google.com/open?id=13Q-bBkVlAX7uEweDyQGvNct-dXkBSveT

iOS Interactive Animation Swift

I cannot for the life understand why this is not working.
Have two VCs: A and B.
I want to swipe right on VC A to reveal VC B but want to make it interactive so that a user can drag between two VCs (like on Instagram home screen when you swipe left and right to go to the Camera and messages). At the moment, it doesn't 'drag'. You can swipe on VC A and it will go to VC B.
Here's my animator object to slide right:
class SlideRightTransitionAnimator: NSObject {
let duration = 0.5
var isPresenting = false
let customInteractiveTransition = CustomInteractiveTransition()
}
// MARK: UIViewControllerTransitioningDelegate
extension SlideRightTransitionAnimator: UIViewControllerTransitioningDelegate {
// Return the animator when presenting a VC
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = true
return self
}
// Return the animator used when dismissing from a VC
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = false
return self
}
// Add the interactive transition for Presentation only
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return customInteractiveTransition.transitionInProgress ? customInteractiveTransition : nil
}
}
// MARK: UIViewControllerTransitioningDelegate
extension SlideRightTransitionAnimator: UIViewControllerAnimatedTransitioning {
// Return how many seconds the transiton animation will take
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
// Animate a change from one VC to another
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get reference to our fromView, toView and the container view that we should perform the transition
let container = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
// Set up the transform we'll use in the animation
let offScreenRight = CGAffineTransform(translationX: container.frame.width, y: 0)
let offScreenLeft = CGAffineTransform(translationX: -container.frame.width, y: 0)
// Start the toView to the right of the screen
if isPresenting {
toView.transform = offScreenLeft
}
// Add the both views to our VC
container.addSubview(toView)
container.addSubview(fromView)
// Perform the animation
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: [], animations: {
if self.isPresenting {
fromView.transform = offScreenRight
toView.transform = CGAffineTransform.identity
}
else {
fromView.transform = offScreenLeft
toView.transform = CGAffineTransform.identity
}
}, completion: { finished in
// Tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
}
Here's my Interactive Transition Controller
class CustomInteractiveTransition: UIPercentDrivenInteractiveTransition {
weak var viewController : UIViewController!
var shouldCompleteTransition = false
var transitionInProgress = false
var completionSeed: CGFloat {
return 1 - percentComplete
}
func attachToViewController(viewController: UIViewController) {
self.viewController = viewController
setupPanGestureRecognizer(view: viewController.view)
}
private func setupPanGestureRecognizer(view: UIView) {
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)))
}
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
let viewTranslation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
var progress = (viewTranslation.x / 200)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
transitionInProgress = true
viewController.dismiss(animated: true, completion: nil)
case .changed:
shouldCompleteTransition = progress > 0.5
update(progress)
case .cancelled, .ended:
transitionInProgress = false
if !shouldCompleteTransition || gestureRecognizer.state == .cancelled {
cancel()
} else {
finish()
}
default:
print("Swift switch must be exhaustive, thus the default")
}
}
}
And lastly the code for VC A:
class ViewControllerA: UITableViewController {
let slideRightTransition = SlideRightTransitionAnimator()
let customInteractiveTransition = CustomInteractiveTransition()
override func viewDidLoad() {
super.viewDidLoad()
// Add a Pan Gesture to swipe to other VC
let swipeGestureRight = UISwipeGestureRecognizer(target: self, action: #selector(swipeGestureRightAction))
swipeGestureRight.direction = .right
view.addGestureRecognizer(swipeGestureRight)
}
// MARK: Pan gestures
func swipeGestureRightAction() {
performSegue(withIdentifier: "showMapSegue", sender: self)
}
// MARK: Prepare for segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showVCB" {
// This gets a reference to the screen that we're about to transition to and from
let toViewController = segue.destination as UIViewController
// Instead of using the default transition animation, we'll ask the segue to use our custom SlideRightTransitionAnimator object to manage the transition animation
toViewController.transitioningDelegate = slideRightTransition
// Add the Interactive gesture transition to the VC
customInteractiveTransition.attachToViewController(viewController: toViewController)
}
}
Thank you in advance!!!
In your viewDidLoad of VC A, you'll want to replace that UISwipeGestureRecognizer with a UIPanGestureRecognizer. Then implement the appropriate .changed state in the gesture handler function.
EDIT: Moreover, to achieve a sliding transition between controllers, I highly recommend using a UIPageViewController instead. That or maybe even a custom UICollectionView solution.
in function handlePanGesture you are taking translation reference from VC A (i.e, gestureRecognizer.view!.superview!)
but instead of that take referance from UIWindow. (i.e, UIApplication.shared.windows.last)
replace gestureRecognizer.view!.superview! with UIApplication.shared.windows.last
it's worked for me.

Resources