如何实现“向右拖动以关闭”导航堆栈中的视图控制器?

Posted

技术标签:

【中文标题】如何实现“向右拖动以关闭”导航堆栈中的视图控制器?【英文标题】:How can I implement "drag right to dismiss" a View Controller that's in a navigation stack? 【发布时间】:2016-02-14 06:18:13 【问题描述】:

默认情况下,如果你从屏幕的左边缘向右拖动,它会拖走 ViewController 并将其从堆栈中移除。

我想将此功能扩展到整个屏幕。当用户在任何地方拖动时,我希望发生同样的事情。

我知道我可以实现向右滑动手势,只需调用self.navigationController?.popViewControllerAnimated(true)

但是,没有“拖动”动作。我希望用户能够像对象一样右键拖动视图控制器,从而显示下面的内容。并且,如果它拖到 50% 以上,请忽略它。 (查看 Instagram 以了解我的意思。)

【问题讨论】:

我认为这对你有帮助developer.apple.com/library/ios/documentation/UIKit/Reference/… Instagram 是否将其排除在 33% 以上?我刚刚尝试过,但在我看来,有 50% 的人会忽略这个观点。 @Blaszard 刚刚更新。 如果没有赏金,我会因为过于宽泛而结束这个问题。有多个教程(Apple、博客)和多个 github 库。 这些库完全符合您的要求:fastred/SloppySwiper 和 jaredsinclair/JTSSloppySwiping 【参考方案1】:

在 Github 上做了一个演示项目 https://github.com/rishi420/SwipeRightToPopController

我用过UIViewControllerAnimatedTransitioning协议

来自文档:

// 这用于百分比驱动的交互式转换,以及容器控制器...

在控制器的视图中添加了UIPanGestureRecognizer。这是手势的动作:

func handlePanGesture(panGesture: UIPanGestureRecognizer) 

    let percent = max(panGesture.translationInView(view).x, 0) / view.frame.width

    switch panGesture.state 

    case .Began:
        navigationController?.delegate = self
        navigationController?.popViewControllerAnimated(true)

    case .Changed:
        percentDrivenInteractiveTransition.updateInteractiveTransition(percent)

    case .Ended:
        let velocity = panGesture.velocityInView(view).x

        // Continue if drag more than 50% of screen width or velocity is higher than 1000
        if percent > 0.5 || velocity > 1000 
            percentDrivenInteractiveTransition.finishInteractiveTransition()
         else 
            percentDrivenInteractiveTransition.cancelInteractiveTransition()
        

    case .Cancelled, .Failed:
        percentDrivenInteractiveTransition.cancelInteractiveTransition()

    default:
        break
    

步骤:

    计算视图上的拖动百分比 .Begin: 指定要执行的 segue 并分配 UINavigationController 委托。 InteractiveTransitioning 需要委托 .Changed:UpdateInteractiveTransition 与百分比 .Ended: 如果拖动 50% 或更多或更高的速度,则继续剩余过渡,否则取消 .Cancelled, .Failed:取消过渡

参考文献:

    UIPercentDrivenInteractiveTransition https://github.com/visnup/swipe-left https://github.com/robertmryan/ScreenEdgeGestureNavigationController https://github.com/groomsy/custom-navigation-animation-transition-demo

【讨论】:

有什么方法可以做到这一点,而不必重新创建您知道的默认弹出过渡? 步骤 2 case .Begin: 在其中指定 segue。它可以是您选择的任何选择。 我不是这个意思。在您的示例项目中,您创建了一个模仿默认过渡动画的SlideAnimatedTransitioning 类,对吧?我想知道我是否可以简单地使用默认动画,而不必模仿它...... 我如何使用这个例子来 SwipeLeftToPushController ? 这太过分了...您可以将操作复制到平移手势识别器。请参阅下面的答案。【参考方案2】:

创建一个平移手势识别器并移动交互式弹出手势识别器的目标。

将您的识别器添加到推送的视图控制器的 viewDidLoad 中,瞧!

编辑:用更详细的解决方案更新了代码。

import os
import UIKit

public extension UINavigationController 
  func fixInteractivePopGestureRecognizer(delegate: UIGestureRecognizerDelegate) 
    guard
      let popGestureRecognizer = interactivePopGestureRecognizer,
      let targets = popGestureRecognizer.value(forKey: "targets") as? NSMutableArray,
      let gestureRecognizers = view.gestureRecognizers,
      // swiftlint:disable empty_count
      targets.count > 0
    else  return 

    if viewControllers.count == 1 
      for recognizer in gestureRecognizers where recognizer is PanDirectionGestureRecognizer 
        view.removeGestureRecognizer(recognizer)
        popGestureRecognizer.isEnabled = false
        recognizer.delegate = nil
      
     else 
      if gestureRecognizers.count == 1 
        let gestureRecognizer = PanDirectionGestureRecognizer(axis: .horizontal, direction: .right)
        gestureRecognizer.cancelsTouchesInView = false
        gestureRecognizer.setValue(targets, forKey: "targets")
        gestureRecognizer.require(toFail: popGestureRecognizer)
        gestureRecognizer.delegate = delegate
        popGestureRecognizer.isEnabled = true

        view.addGestureRecognizer(gestureRecognizer)
      
    
  


public enum PanAxis 
  case vertical
  case horizontal


public enum PanDirection 
  case left
  case right
  case up
  case down
  case normal


public class PanDirectionGestureRecognizer: UIPanGestureRecognizer 
  let axis: PanAxis
  let direction: PanDirection

  public init(axis: PanAxis, direction: PanDirection = .normal, target: AnyObject? = nil, action: Selector? = nil) 
    self.axis = axis
    self.direction = direction
    super.init(target: target, action: action)
  

  override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) 
    super.touchesMoved(touches, with: event)

    if state == .began 
      let vel = velocity(in: view)
      switch axis 
      case .horizontal where abs(vel.y) > abs(vel.x):
        state = .cancelled
      case .vertical where abs(vel.x) > abs(vel.y):
        state = .cancelled
      default:
        break
      

      let isIncrement = axis == .horizontal ? vel.x > 0 : vel.y > 0

      switch direction 
      case .left where isIncrement:
        state = .cancelled
      case .right where !isIncrement:
        state = .cancelled
      case .up where isIncrement:
        state = .cancelled
      case .down where !isIncrement:
        state = .cancelled
      default:
        break
      
    
  


例如在您的收藏视图中:

  open override func didMove(toParent parent: UIViewController?) 
    navigationController?.fixInteractivePopGestureRecognizer(delegate: self)
  

// MARK: - UIGestureRecognizerDelegate
extension BaseCollection: UIGestureRecognizerDelegate 
  public func gestureRecognizer(
    _ gestureRecognizer: UIGestureRecognizer,
    shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
  ) -> Bool 
    otherGestureRecognizer is PanDirectionGestureRecognizer
  

【讨论】:

我不太明白,但这确实有效,不需要transitionDelegates等。 这是最干净的答案。 这是私有 API? 非常感谢,它非常干净和简单。 @AndreyOshev 它没有使用任何私有 API【参考方案3】:

@Warif Akhand Rishi 接受的答案的 Swift 4 版本

尽管这个答案确实有效,但我发现了 2 个怪癖。

    如果您向左滑动,它也会消失,就像您向右滑动一样。 它也非常微妙,因为即使向任一方向轻轻滑动也会关闭 vc。

除此之外它确实有效,您可以向右或向左滑动以关闭。

class ViewController: UIGestureRecognizerDelegate, UINavigationControllerDelegate 

    override func viewDidLoad() 
        super.viewDidLoad()

        navigationController?.interactivePopGestureRecognizer?.delegate = self
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(panGesture)
    

    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer)

        let interactiveTransition = UIPercentDrivenInteractiveTransition()

        let percent = max(gesture.translation(in: view).x, 0) / view.frame.width

        switch gesture.state 

        case .began:
            navigationController?.delegate = self

            // *** use this if the vc is PUSHED on the stack **
            navigationController?.popViewController(animated: true)

            // *** use this if the vc is PRESENTED **
            //navigationController?.dismiss(animated: true, completion: nil)

        case .changed:
            interactiveTransition.update(percent)

        case .ended:
            let velocity = gesture.velocity(in: view).x

            // Continue if drag more than 50% of screen width or velocity is higher than 1000
            if percent > 0.5 || velocity > 1000 
                interactiveTransition.finish()
             else 
                interactiveTransition.cancel()
            

        case .cancelled, .failed:
            interactiveTransition.cancel()

        default:break
        
    

【讨论】:

您的解决方案正在快速弹出视图控制器 你好@LanceSamaria 有没有办法我可以实现这个来关闭呈现而不是推送的 UIViewController? @IsraelMeshileya 我没有尝试过,但可能会将 navigationController?.popViewController(animated(: true) 的行更改为 dismiss... b> 哦,一点也不。我仍在尝试其他一些调整,但一旦完成会通知您。 @IsraelMeshileya 我将项目添加到 GitHub。只需下载它,运行它,点击按钮,在 myVC 出现后,滑动关闭它就可以正常工作。左上角有一个cancelButton,不需要点击取消。在这里:github.com/lsamaria/SwipeToDismiss。如果它适合你,请告诉我【参考方案4】:

您需要调查UINavigationController 的interactivePopGestureRecognizer 属性。

这是一个类似的问题,带有示例代码来解决这个问题。

UINavigationController interactivePopGestureRecognizer working abnormal in iOS7

【讨论】:

【参考方案5】:

我认为这比建议的解决方案更容易,并且也适用于导航内的所有 viewController 以及嵌套的滚动视图。

https://***.com/a/58779146/8517882

只需安装 pod,然后使用 EZNavigationController 而不是 UINavigationController 即可在该导航控制器内的所有视图控制器上具有此行为。

【讨论】:

【参考方案6】:

答案太复杂了。有一个简单的解决方案。将下一行添加到您想要拥有此功能的基本导航控制器或导航控制器:

  self.interactivePopGestureRecognizer?.delegate = nil

【讨论】:

【参考方案7】:

向右滑动关闭视图控制器

Swift 5 版本 - (也去掉了从右向左滑动时的手势识别)

重要 - 在 VC2 的“属性检查器”中,将“Presentation”值从“Full Screen”设置为“Over Full Screen”。这将允许 VC1 在通过手势关闭 VC2 时可见 — 没有它,VC2 后面会出现黑屏,而不是 VC1。

class ViewController: UIGestureRecognizerDelegate, UINavigationControllerDelegate 
    
    var initialTouchPoint: CGPoint = CGPoint(x: 0, y: 0)
    
    override func viewDidLoad() 
        super.viewDidLoad()

        navigationController?.interactivePopGestureRecognizer?.delegate = self
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(panGesture)
    

    @objc func handlePanGesture(_ sender: UIPanGestureRecognizer) 
        let touchPoint = sender.location(in: self.view?.window)
        let percent = max(sender.translation(in: view).x, 0) / view.frame.width
        let velocity = sender.velocity(in: view).x
        
        if sender.state == UIGestureRecognizer.State.began 
            initialTouchPoint = touchPoint
         else if sender.state == UIGestureRecognizer.State.changed 
            if touchPoint.x - initialTouchPoint.x > 0 
                self.view.frame = CGRect(x: touchPoint.x - initialTouchPoint.x, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
            
         else if sender.state == UIGestureRecognizer.State.ended || sender.state == UIGestureRecognizer.State.cancelled 
            
            if percent > 0.5 || velocity > 1000 
                navigationController?.popViewController(animated: true)
             else 
                UIView.animate(withDuration: 0.3, animations: 
                    self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
                )
            
        
    

【讨论】:

以上是关于如何实现“向右拖动以关闭”导航堆栈中的视图控制器?的主要内容,如果未能解决你的问题,请参考以下文章

如何从导航堆栈中的任何位置弹出到任意视图控制器

如何快速隐藏嵌入在导航堆栈中的视图控制器中的选项卡栏?

视图是黑色的,当返回到导航控制器堆栈视图时,在实现自定义转换后

在导航堆栈中以模态方式呈现视图控制器

子视图控制器中的导航堆栈

从导航堆栈中删除视图控制器