Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swipe to back (Navigation) #343

Closed
Sorix opened this issue Dec 1, 2017 · 23 comments
Closed

Swipe to back (Navigation) #343

Sorix opened this issue Dec 1, 2017 · 23 comments

Comments

@Sorix
Copy link

Sorix commented Dec 1, 2017

That question was already raised before. How can I enable swipe to back if hero was used within navigationController?.pushViewController? Hero disables swipes now, so application become very unintuitive without default swipe gestures in navigation controllers.

@rmurdoch
Copy link

rmurdoch commented Dec 2, 2017

Here's a swift class I use for swipe back.

import UIKit
import Hero

class HeroHelper: NSObject {
  
    func configureHero(in navigationController: UINavigationController) {
        guard let topViewController = navigationController.topViewController else { return }

        topViewController.isHeroEnabled = true
        navigationController.heroNavigationAnimationType = .fade
        navigationController.isHeroEnabled = true
        navigationController.delegate = self
    }
}

//Navigation Popping
private extension HeroHelper {
    private func addEdgePanGesture(to view: UIView) {
        let pan = UIScreenEdgePanGestureRecognizer(target: self, action:#selector(popViewController(_:)))
        pan.edges = .left
        view.addGestureRecognizer(pan)
    }
    
    @objc private func popViewController(_ gesture: UIScreenEdgePanGestureRecognizer) {
        guard let view = gesture.view else { return }
        let translation = gesture.translation(in: nil)
        let progress = translation.x / 2 / view.bounds.width
        
        switch gesture.state {
        case .began:
            UIViewController.firstViewController.hero_dismissViewController()
        case .changed:
            Hero.shared.update(progress)
        default:
            if progress + gesture.velocity(in: nil).x / view.bounds.width > 0.3 {
                Hero.shared.finish()
            } else {
                Hero.shared.cancel()
            }
        }
    }
}


//Navigation Controller Delegate
extension HeroHelper: UINavigationControllerDelegate {
    
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return Hero.shared.navigationController(navigationController, interactionControllerFor: animationController)
    }
    
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if operation == .push {
            addEdgePanGesture(to: toVC.view)
        }
        return Hero.shared.navigationController(navigationController, animationControllerFor: operation, from: fromVC, to: toVC)
    }
} 

@gregoireLem
Copy link

Do you guys have an exemple ? I got the error : Type 'UIViewController' has no member 'firstViewController'
thanks

@rmurdoch
Copy link

rmurdoch commented Jan 5, 2018

UIViewController.firstViewController is a UIViewController extension to grab the top most view controller within the app.

@ivanvorobei
Copy link

@rmurdoch please, you can describe how your swift class use?

@rmurdoch
Copy link

rmurdoch commented Jan 8, 2018

@ivanvorobei, what do you need me to describe, the whole class or the how its used?

@ivanvorobei
Copy link

@rmurdoch how it used

@rmurdoch
Copy link

rmurdoch commented Jan 8, 2018

@ivanvorobei, its used to add the swipe back navigation and hero animated transition. When a new VC is added to the navigation controller, it adds the swipe back gesture. When the swipe occurs, it updates the hero transition for the animation.

@Mahdimm
Copy link

Mahdimm commented Feb 25, 2018

@rmurdoch thanks for you're help. but for usage we have to set delegate for navigation controller after that. It does default transition what did I wrong?

@rmurdoch
Copy link

You have to set the navigation delegate to the HeroHelper object, otherwise if you set navigationController.isHeroEnabled = true, the navigation delegate will be set to the hero framework.

@junweimah
Copy link

junweimah commented Mar 23, 2018

@rmurdoch

UIViewController.firstViewController is a UIViewController extension to grab the top most view controller within the app.

Do you have this extension? Can share the code?

You have to set the navigation delegate to the HeroHelper object

I try setting this in my UIViewController using this line, but I am getting error :

self.navigationController?.delegate = HeroHelper

And how do we call addEdgePanGesture? Do I need to add the call to this function in the configureHero function in class HeroHelper: NSObject?

Thanks

@emrekaranfil
Copy link

panGR = UIPanGestureRecognizer(target: self, action: #selector(leftSwipeDismiss(gestureRecognizer:)))        
view.addGestureRecognizer(panGR)
@objc func leftSwipeDismiss(gestureRecognizer:UIPanGestureRecognizer) {
     
     let translation = panGR.translation(in: nil)
     let progress = translation.x / 2 / view.bounds.width
     let gestureView = gestureRecognizer.location(in: self.view)
     
     switch panGR.state {
     case .began:
         
         if gestureView.x <= 30 {
             hero_dismissViewController()
         }

     case .changed:
         
         let translation = panGR.translation(in: nil)
         let progress = translation.x / 2 / view.bounds.width
         Hero.shared.update(progress)
         
     default:
         if progress + panGR.velocity(in: nil).x / view.bounds.width > 0.3 {
             Hero.shared.finish()
         } else {
             Hero.shared.cancel()
         }
     }
     
 }

@runryan
Copy link

runryan commented May 26, 2018

When use hero we need to handle swipe back gesture by ourself. Based on @emrekaranfil & @rmurdoch 's answers, I create a BaseViewController class first and all the controllers within NavigationController inherit it. In viewDidLoad() method making sure the opened controller is at top and only the top controller can deal with PanGesture.

class BaseViewController: BaseViewController {
    
    private  lazy var panGR: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(leftSwipeDismiss(gestureRecognizer:)))
    
    public var enableScreenPanGesture: Bool = true {
        didSet {
            if(enableScreenPanGesture) {
                self.view.addGestureRecognizer(self.panGR)
                return
            }
            if self.view.gestureRecognizers?.contains(panGR) ?? false {
                self.view.removeGestureRecognizer(panGR)
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        enableScreenPanGesture = navigationController?.viewControllers.count ?? 0 > 1 && navigationController?.viewControllers.last == self
        hero.isEnabled = true
    }

  
    @objc func leftSwipeDismiss(gestureRecognizer: UIPanGestureRecognizer) {
        
        let translation = gestureRecognizer.translation(in: nil)
        let progress = translation.x / 2 / view.bounds.width
        let gestureView = gestureRecognizer.location(in: self.view)
        
        switch gestureRecognizer.state {
        case .began:
            if gestureView.x <= 30 {
                hero.dismissViewController()
            }
            
        case .changed:
            let translation = gestureRecognizer.translation(in: nil)
            let progress = translation.x / 2 / view.bounds.width
            Hero.shared.update(progress)
            
        default:
            if progress + gestureRecognizer.velocity(in: nil).x / view.bounds.width > 0.3 {
                Hero.shared.finish()
                return
            }
            Hero.shared.cancel()
        }
        
    }
}

The code above supports swipe back and hero at the same time. if you don't want swipe back, set enableScreenPanGesture = false

Attention: This is not a good way fixing this problem. Be careful to use it your own project.

@edrobe
Copy link

edrobe commented Sep 19, 2018

@runryan amazing but when I set 'navigationController.hero.navigationAnimationType = .zoomOut' by your code it become invalid, what should I do?

@runryan
Copy link

runryan commented Sep 19, 2018

@edrobe Sorry, I'm not expert at Hero. Even the code above I suggest not using it. It may encounter unknown bugs. Hero is amazing, but I've stopped using it for the moment.

@edrobe
Copy link

edrobe commented Sep 20, 2018

@runryan Thank you for the reply. I found the answer by myself this morning. Your code is good. And I just use 'navigationController?.hero.isEnabled = true' instead of your 'hero.isEnabled = true' and then hero shows the custom animation successfully. I'll show the code below.

@edrobe
Copy link

edrobe commented Sep 20, 2018

import Hero

class BaseViewController: UIViewController {

private  lazy var panGR: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(leftSwipeDismiss(gestureRecognizer:)))


public var enableScreenPanGesture: Bool = true {
    didSet {
        if(enableScreenPanGesture) {
            self.view.addGestureRecognizer(self.panGR)
            return
        }
        if self.view.gestureRecognizers?.contains(panGR) ?? false {
            self.view.removeGestureRecognizer(panGR)
        }
    }
}


override open func viewDidLoad() {
    super.viewDidLoad()
    
    enableScreenPanGesture = navigationController?.viewControllers.count ?? 0 > 1 && navigationController?.viewControllers.last == self
    
    // here using .autoReverse to generate the suitable come-back animation by hero.
    navigationController?.hero.navigationAnimationType = .autoReverse(presenting: .zoomOut)
    
    navigationController?.hero.isEnabled = true
    
}


@objc func leftSwipeDismiss(gestureRecognizer: UIPanGestureRecognizer) {
    
    let translation = gestureRecognizer.translation(in: nil)
    let progress = translation.x / 2 / view.bounds.width
    let gestureView = gestureRecognizer.location(in: self.view)
    
    switch gestureRecognizer.state {
    case .began:
        if gestureView.x <= 30 {
            hero.dismissViewController()
        }
        
    case .changed:
        let translation = gestureRecognizer.translation(in: nil)
        let progress = translation.x / 2 / view.bounds.width
        Hero.shared.update(progress)
        
    default:
        if progress + gestureRecognizer.velocity(in: nil).x / view.bounds.width > 0.3 {
            Hero.shared.finish()
            return
        }
        Hero.shared.cancel()
    }
    
}

}

@jdanthinne
Copy link

@edrobe Nice solution, but unfortunately, it disables some interactions (ie. a UITableView doesn't receive the swipe to delete event).

@kuyazee
Copy link
Member

kuyazee commented May 9, 2019

Here's what I did to the HeroHelper class that @rmurdoch did, and it works for me

class HeroHelper: NSObject {
    let navigationController: UINavigationController

    required init(navigationController: UINavigationController) {
        self.navigationController = navigationController
        super.init()
        self.navigationController.hero.isEnabled = true
        self.navigationController.hero.navigationAnimationType = .fade
        self.navigationController.delegate = self
    }
}

// Navigation Popping
extension HeroHelper {
    private func addEdgePanGesture(to view: UIView) {
        let pan = UIScreenEdgePanGestureRecognizer(
            target: self,
            action: #selector(self.popViewController(_:))
        )
        pan.edges = .left
        view.addGestureRecognizer(pan)
    }

    @objc private func popViewController(_ gesture: UIScreenEdgePanGestureRecognizer) {
        guard let view = gesture.view else { return }
        let translation = gesture.translation(in: nil)
        let progress = translation.x / 2 / view.bounds.width

        switch gesture.state {
        case .began:
            self.navigationController.topViewController?.hero.dismissViewController()
        case .changed:
            Hero.shared.update(progress)
        default:
            if progress + gesture.velocity(in: nil).x / view.bounds.width > 0.3 {
                Hero.shared.finish()
            } else {
                Hero.shared.cancel()
            }
        }
    }
}

// Navigation Controller Delegate
extension HeroHelper: UINavigationControllerDelegate {
    func navigationController(
        _ navigationController: UINavigationController,
        interactionControllerFor animationController: UIViewControllerAnimatedTransitioning
    ) -> UIViewControllerInteractiveTransitioning? {
        return Hero.shared.navigationController(navigationController, interactionControllerFor: animationController)
    }

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        if navigationController.viewControllers.count > 1 {
            self.addEdgePanGesture(to: viewController.view)
        }
    }

    func navigationController(
        _ navigationController: UINavigationController,
        animationControllerFor operation: UINavigationController.Operation,
        from fromVC: UIViewController,
        to toVC: UIViewController
    ) -> UIViewControllerAnimatedTransitioning? {
        return Hero.shared.navigationController(navigationController, animationControllerFor: operation, from: fromVC, to: toVC)
    }
}

@alouanemed
Copy link

@kuyazee Where do you use that helper? Thanks.

@kuyazee
Copy link
Member

kuyazee commented Jun 3, 2019

@alouanemed It's instantiated and kept on reference on the root UINavigationController.

@alouanemed
Copy link

Thanks, but where can I find my root UINavigationController.?

@kleinerQ
Copy link

kleinerQ commented Aug 6, 2019

hii, following is my solution, just add gesture to the self.view

 @objc func leftSwipeDismiss(gestureRecognizer:UIPanGestureRecognizer) {

        switch gestureRecognizer.state {
        case .began:
            hero.dismissViewController()
        case .changed:
            
            let translation = gestureRecognizer.translation(in: nil)
            let progress = translation.x / 2.0 / view.bounds.width
            Hero.shared.update(progress)
            Hero.shared.apply(modifiers: [.translate(x: translation.x)], to: self.view)
            break
        default:
            let translation = gestureRecognizer.translation(in: nil)
            let progress = translation.x / 2.0 / view.bounds.width
            if progress + gestureRecognizer.velocity(in: nil).x / view.bounds.width > 0.3 {
                Hero.shared.finish()
            } else {
                Hero.shared.cancel()
            }
        }
        
    }

@caosuyang
Copy link

HeroHelper这就是我对套件所做的@rmurdoch做到了,这对我有用

class HeroHelper: NSObject {
    let navigationController: UINavigationController

    required init(navigationController: UINavigationController) {
        self.navigationController = navigationController
        super.init()
        self.navigationController.hero.isEnabled = true
        self.navigationController.hero.navigationAnimationType = .fade
        self.navigationController.delegate = self
    }
}

// Navigation Popping
extension HeroHelper {
    private func addEdgePanGesture(to view: UIView) {
        let pan = UIScreenEdgePanGestureRecognizer(
            target: self,
            action: #selector(self.popViewController(_:))
        )
        pan.edges = .left
        view.addGestureRecognizer(pan)
    }

    @objc private func popViewController(_ gesture: UIScreenEdgePanGestureRecognizer) {
        guard let view = gesture.view else { return }
        let translation = gesture.translation(in: nil)
        let progress = translation.x / 2 / view.bounds.width

        switch gesture.state {
        case .began:
            self.navigationController.topViewController?.hero.dismissViewController()
        case .changed:
            Hero.shared.update(progress)
        default:
            if progress + gesture.velocity(in: nil).x / view.bounds.width > 0.3 {
                Hero.shared.finish()
            } else {
                Hero.shared.cancel()
            }
        }
    }
}

// Navigation Controller Delegate
extension HeroHelper: UINavigationControllerDelegate {
    func navigationController(
        _ navigationController: UINavigationController,
        interactionControllerFor animationController: UIViewControllerAnimatedTransitioning
    ) -> UIViewControllerInteractiveTransitioning? {
        return Hero.shared.navigationController(navigationController, interactionControllerFor: animationController)
    }

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        if navigationController.viewControllers.count > 1 {
            self.addEdgePanGesture(to: viewController.view)
        }
    }

    func navigationController(
        _ navigationController: UINavigationController,
        animationControllerFor operation: UINavigationController.Operation,
        from fromVC: UIViewController,
        to toVC: UIViewController
    ) -> UIViewControllerAnimatedTransitioning? {
        return Hero.shared.navigationController(navigationController, animationControllerFor: operation, from: fromVC, to: toVC)
    }
}

I used .slide(direction: .right) and .slide(direction: .left) respectively when pushing the two pages, but I couldn't unify the direction of the gesture and hero pop return animation, such as unifying it to the left. Sliding the page while panning to the left. For example, sliding the page to the right while panning to the right. This is a difficult problem I am encountering now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests