HeroTransitions / Hero

Elegant transition library for iOS & tvOS
https://HeroTransitions.github.io/Hero/
MIT License
22.06k stars 1.73k forks source link

Swipe to back (Navigation) #343

Closed Sorix closed 2 years ago

Sorix commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

rmurdoch commented 6 years ago

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

ivanvorobei commented 6 years ago

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

rmurdoch commented 6 years ago

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

ivanvorobei commented 6 years ago

@rmurdoch how it used

rmurdoch commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago
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 commented 6 years ago

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 commented 6 years ago

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

runryan commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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

kuyazee commented 5 years ago

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 commented 5 years ago

@kuyazee Where do you use that helper? Thanks.

kuyazee commented 5 years ago

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

alouanemed commented 5 years ago

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

kleinerQ commented 5 years ago

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 commented 6 months ago

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.