Closed Sorix closed 2 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)
}
}
Do you guys have an exemple ? I got the error : Type 'UIViewController' has no member 'firstViewController' thanks
UIViewController.firstViewController is a UIViewController extension to grab the top most view controller within the app.
@rmurdoch please, you can describe how your swift class use?
@IvanVorobei, what do you need me to describe, the whole class or the how its used?
@rmurdoch how it used
@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.
@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?
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.
@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
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()
}
}
}
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.
@runryan amazing but when I set 'navigationController.hero.navigationAnimationType = .zoomOut' by your code it become invalid, what should I do?
@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.
@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.
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()
}
}
}
@edrobe Nice solution, but unfortunately, it disables some interactions (ie. a UITableView doesn't receive the swipe to delete event).
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)
}
}
@kuyazee Where do you use that helper? Thanks.
@alouanemed It's instantiated and kept on reference on the root UINavigationController.
Thanks, but where can I find my root UINavigationController.?
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()
}
}
}
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.
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.