【问题标题】:In iOS, how to drag down to dismiss a modal?在 iOS 中,如何向下拖动以关闭模式?
【发布时间】:2015-05-31 04:03:24
【问题描述】:

关闭模态框的常用方法是向下滑动 - 我们如何允许用户向下拖动模态框,如果它足够远,模态框将被关闭,否则它会动画回到原始位置?

例如,我们可以在 Twitter 应用的照片视图或 Snapchat 的“发现”模式中找到它。

类似的线程指出,当用户向下滑动时,我们可以使用 UISwipeGestureRecognizer 和 [self dismissViewControllerAnimated...] 来关闭模态 VC。但这只能处理一次滑动,而不是让用户拖动模态框。

【问题讨论】:

标签: ios viewcontroller gesture-recognition


【解决方案1】:

我将分享我在 Swift 3 中是如何做到的:

结果

实施

class MainViewController: UIViewController {

  @IBAction func click() {
    performSegue(withIdentifier: "showModalOne", sender: nil)
  }
  
}

class ModalOneViewController: ViewControllerPannable {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .yellow
  }
  
  @IBAction func click() {
    performSegue(withIdentifier: "showModalTwo", sender: nil)
  }
}

class ModalTwoViewController: ViewControllerPannable {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .green
  }
}

模态视图控制器继承自我构建的 class (ViewControllerPannable) 以使它们在达到一定速度时可拖动和关闭。

ViewControllerPannaable 类

class ViewControllerPannable: UIViewController {
  var panGestureRecognizer: UIPanGestureRecognizer?
  var originalPosition: CGPoint?
  var currentPositionTouched: CGPoint?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
    view.addGestureRecognizer(panGestureRecognizer!)
  }
  
  @objc func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translation(in: view)
    
    if panGesture.state == .began {
      originalPosition = view.center
      currentPositionTouched = panGesture.location(in: view)
    } else if panGesture.state == .changed {
        view.frame.origin = CGPoint(
          x: translation.x,
          y: translation.y
        )
    } else if panGesture.state == .ended {
      let velocity = panGesture.velocity(in: view)

      if velocity.y >= 1500 {
        UIView.animate(withDuration: 0.2
          , animations: {
            self.view.frame.origin = CGPoint(
              x: self.view.frame.origin.x,
              y: self.view.frame.size.height
            )
          }, completion: { (isCompleted) in
            if isCompleted {
              self.dismiss(animated: false, completion: nil)
            }
        })
      } else {
        UIView.animate(withDuration: 0.2, animations: {
          self.view.center = self.originalPosition!
        })
      }
    }
  }
}

【讨论】:

  • 我复制了你的代码,它成功了。下拉时模型视图的背景是黑色的,不像你的那样透明
  • 在情节提要中,从MainViewControllerStoryboard segueAttributes Inspector 面板到ModalViewController:将Presentation 属性设置为在当前上下文中
  • 这似乎比公认的答案更容易,但我在 ViewControllerPannaable 类中遇到错误。错误是“无法调用非函数类型 UIPanGestureRecognizer 的值”。它就在“panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))”行有什么想法吗?
  • 修复了我提到的错误,将其更改为 UIPanGestureRecognizer。 "panGestureRecognizer = panGestureRecognizer(target..." 改为:"panGestureRecognizer = UIPanGestureRecognizer(target..."
  • 由于我展示的是 VC 而不是模态展示它,如何在关闭时移除黑色背景?
【解决方案2】:

您所描述的是一个交互式自定义过渡动画。您正在自定义动画和转换的驱动手势,即解除(或不解除)呈现的视图控制器。实现它的最简单方法是将 UIPanGestureRecognizer 与 UIPercentDrivenInteractiveTransition 结合使用。

我的书解释了如何做到这一点,并且我已经发布了示例(来自书中)。这个特殊的例子是一种不同的情况——过渡是横向的,而不是向下的,它是针对一个标签栏控制器,而不是一个呈现的控制器——但基本的想法是完全一样的:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p300customAnimation3/ch19p620customAnimation1/Animator.swift

如果您下载该项目并运行它,您将看到正在发生的事情正是您所描述的,除了它是横向的:如果拖动超过一半,我们过渡,但如果不是,我们取消并弹回原位。

【讨论】:

  • 404 页面未找到。
【解决方案3】:

对于那些真正想深入了解自定义 UIViewController 转换的人,我推荐this great tutorial from raywenderlich.com

原始的最终示例项目包含错误。所以我修复了它并上传到Github repo。该项目在 Swift 5 中,因此您可以轻松运行和播放它。

这是预览:

而且它也是交互式的!

黑客愉快!

【讨论】:

    【解决方案4】:

    我刚刚创建了一个教程,用于交互式地向下拖动模式以将其关闭。

    http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/

    一开始我发现这个主题令人困惑,因此本教程将逐步构建此主题。

    如果你只想自己运行代码,这里是 repo:

    https://github.com/ThornTechPublic/InteractiveModal

    这是我使用的方法:

    视图控制器

    您使用自定义动画覆盖关闭动画。如果用户正在拖动模式,interactor 就会启动。

    import UIKit
    
    class ViewController: UIViewController {
        let interactor = Interactor()
        override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
            if let destinationViewController = segue.destinationViewController as? ModalViewController {
                destinationViewController.transitioningDelegate = self
                destinationViewController.interactor = interactor
            }
        }
    }
    
    extension ViewController: UIViewControllerTransitioningDelegate {
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
           DismissAnimator()
        }
        func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
           interactor.hasStarted ? interactor : .none
        }
    }
    

    解雇动画师

    您创建了一个自定义动画师。这是您在 UIViewControllerAnimatedTransitioning 协议中打包的自定义动画。

    import UIKit
    
    class DismissAnimator : NSObject {
       let transitionDuration = 0.6
    }
    
    extension DismissAnimator : UIViewControllerAnimatedTransitioning {
        func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
           transitionDuration
        }
        
        func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
            guard
                let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
                let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
                let containerView = transitionContext.containerView()
                else {
                    return
            }
            if transitionContext.transitionWasCancelled {
              containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
            }
            let screenBounds = UIScreen.mainScreen().bounds
            let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
            let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
            
            UIView.animateWithDuration(
                transitionDuration(transitionContext),
                animations: {
                    fromVC.view.frame = finalFrame
                },
                completion: { _ in
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
                }
            )
        }
    }
    

    交互者

    您将UIPercentDrivenInteractiveTransition 子类化,以便它可以充当您的状态机。由于两个 VC 都访问了交互器对象,因此使用它来跟踪平移进度。

    import UIKit
    
    class Interactor: UIPercentDrivenInteractiveTransition {
        var hasStarted = false
        var shouldFinish = false
    }
    

    模态视图控制器

    这会将平移手势状态映射到交互器方法调用。 translationInView() y 值确定用户是否超过阈值。当平移手势为.Ended 时,交互器要么完成要么取消。

    import UIKit
    
    class ModalViewController: UIViewController {
    
        var interactor:Interactor? = nil
        
        @IBAction func close(sender: UIButton) {
            dismiss(animated: true)
        }
    
        @IBAction func handleGesture(sender: UIPanGestureRecognizer) {
            let percentThreshold:CGFloat = 0.3
            
            let translation = sender.translation(in: view)
            let verticalMovement = translation.y / view.bounds.height
            let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
            let downwardMovementPercent = fminf(downwardMovement, 1.0)
            let progress = CGFloat(downwardMovementPercent)
            guard interactor = interactor else { return }
    
            switch sender.state {
            case .began:
              interactor.hasStarted = true
              dismiss(animated: true)
            case .changed:
              interactor.shouldFinish = progress > percentThreshold
              interactor.update(progress)
            case .cancelled:
              interactor.hasStarted = false
              interactor.cancel()
            case .ended:
              interactor.hasStarted = false
              interactor.shouldFinish ? interactor.finish() : 
              interactor.cancel()
            default:
             break
           }
        }
        
    }
    

    【讨论】:

    • 嘿罗伯特,干得好。您知道我们如何修改它以使其与表视图一起使用吗?也就是说,当tableview位于顶部时能够下拉关闭?谢谢
    • Ross,我创建了一个新分支,其中有一个工作示例:github.com/ThornTechPublic/InteractiveModal/tree/Ross。如果您想先看看它的样子,请查看此 GIF:raw.githubusercontent.com/ThornTechPublic/InteractiveModal/…。表格视图有一个内置的 panGestureRecognizer,可以通过 target-action 连接到现有的 handleGesture(_:) 方法。为了避免与正常的表格滚动冲突,下拉解除仅在表格滚动到顶部时才开始。我使用了快照,并添加了很多 cmets。
    • 罗伯特,更棒的工作。我确实做了自己的实现,它使用了现有的 tableView 平移方法,如 scrollViewDidScroll、scrollViewWillBeginDragging。它要求 tableView 将bounces 和bouncesVertically 都设置为true - 这样我们就可以测量tableview 项目的ContentOffset。这种方法的优点是,如果有足够的速度(由于弹跳),它似乎允许表格视图以一个手势从屏幕上滑出。我可能会在本周的某个时候向您发送拉取请求,这两个选项似乎都有效。
    • 干得好@RossBarbish,我迫不及待地想看看你是如何做到的。能够向上滚动,然后进入交互式过渡模式,这一切都在一个流畅的动作中完成。
    • segue的presentation属性设置为over current context,避免下拉viewController后黑屏
    【解决方案5】:

    这是基于@wilson 的回答(感谢?)的单文件解决方案,并进行了以下改进:


    上一个解决方案的改进列表

    • 限制平移以使视图仅下降:
      • 通过仅更新view.frame.originy 坐标来避免水平平移
      • 使用let y = max(0, translation.y) 向上滑动时避免平移出屏幕
    • 还可以根据手指释放的位置(默认为屏幕的下半部分)关闭视图控制器,而不仅仅是根据滑动速度
    • 将视图控制器显示为模态以确保前一个视图控制器出现在后面并避免黑色背景(应该回答您的问题@nguyễn-anh-việt)
    • 删除不需要的currentPositionTouchedoriginalPosition
    • 公开以下参数:
      • minimumVelocityToHide: 什么速度可以隐藏(默认1500)
      • minimumScreenRatioToHide: 多低才能隐藏(默认为 0.5)
      • animationDuration : 我们隐藏/显示的速度有多快(默认为 0.2 秒)

    解决方案

    斯威夫特 3 和斯威夫特 4:

    //
    //  PannableViewController.swift
    //
    
    import UIKit
    
    class PannableViewController: UIViewController {
        public var minimumVelocityToHide: CGFloat = 1500
        public var minimumScreenRatioToHide: CGFloat = 0.5
        public var animationDuration: TimeInterval = 0.2
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Listen for pan gesture
            let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
            view.addGestureRecognizer(panGesture)
        }
    
        @objc func onPan(_ panGesture: UIPanGestureRecognizer) {
    
            func slideViewVerticallyTo(_ y: CGFloat) {
                self.view.frame.origin = CGPoint(x: 0, y: y)
            }
    
            switch panGesture.state {
    
            case .began, .changed:
                // If pan started or is ongoing then
                // slide the view to follow the finger
                let translation = panGesture.translation(in: view)
                let y = max(0, translation.y)
                slideViewVerticallyTo(y)
    
            case .ended:
                // If pan ended, decide it we should close or reset the view
                // based on the final position and the speed of the gesture
                let translation = panGesture.translation(in: view)
                let velocity = panGesture.velocity(in: view)
                let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
                              (velocity.y > minimumVelocityToHide)
    
                if closing {
                    UIView.animate(withDuration: animationDuration, animations: {
                        // If closing, animate to the bottom of the view
                        self.slideViewVerticallyTo(self.view.frame.size.height)
                    }, completion: { (isCompleted) in
                        if isCompleted {
                            // Dismiss the view when it dissapeared
                            dismiss(animated: false, completion: nil)
                        }
                    })
                } else {
                    // If not closing, reset the view to the top
                    UIView.animate(withDuration: animationDuration, animations: {
                        slideViewVerticallyTo(0)
                    })
                }
    
            default:
                // If gesture state is undefined, reset the view to the top
                UIView.animate(withDuration: animationDuration, animations: {
                    slideViewVerticallyTo(0)
                })
    
            }
        }
    
        override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
            super.init(nibName: nil, bundle: nil)
            modalPresentationStyle = .overFullScreen;
            modalTransitionStyle = .coverVertical;
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            modalPresentationStyle = .overFullScreen;
            modalTransitionStyle = .coverVertical;
        }
    }
    

    【讨论】:

    • 在您的代码中有错字或缺少变量“minimumHeightRatioToHide”
    • 感谢@shokaveli,已修复(原为minimumScreenRatioToHide
    • 这是一个非常好的解决方案。但是,我有一个小问题,不太确定原因是什么:dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 红色背景是模态 VC 的一部分,蓝色背景是呈现模态的 VC 的一部分。当平移手势识别器启动时,我似乎无法修复这种故障行为。
    • 嗨@Knolraap。可能在第一次调用sliceViewVerticallyTo 之前先看一下self.view.frame.origin 的值:我们看到的偏移量似乎与状态栏高度相同,所以您的初始原点可能不是0?
    • 最好在onPan中使用slideViewVerticallyTo作为嵌套函数。
    【解决方案6】:

    这是我的从轴拖动 ViewController 的简单类。只是从 DraggableViewController继承你的类。

    MyCustomClass: DraggableViewController
    

    仅适用于呈现的 ViewController。

    // MARK: - DraggableViewController
    
    public class DraggableViewController: UIViewController {
    
        public let percentThresholdDismiss: CGFloat = 0.3
        public var velocityDismiss: CGFloat = 300
        public var axis: NSLayoutConstraint.Axis = .horizontal
        public var backgroundDismissColor: UIColor = .black {
            didSet {
                navigationController?.view.backgroundColor = backgroundDismissColor
            }
        }
    
        // MARK: LifeCycle
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrag(_:))))
        }
    
        // MARK: Private methods
    
        @objc fileprivate func onDrag(_ sender: UIPanGestureRecognizer) {
    
            let translation = sender.translation(in: view)
    
            // Movement indication index
            let movementOnAxis: CGFloat
    
            // Move view to new position
            switch axis {
            case .vertical:
                let newY = min(max(view.frame.minY + translation.y, 0), view.frame.maxY)
                movementOnAxis = newY / view.bounds.height
                view.frame.origin.y = newY
    
            case .horizontal:
                let newX = min(max(view.frame.minX + translation.x, 0), view.frame.maxX)
                movementOnAxis = newX / view.bounds.width
                view.frame.origin.x = newX
            }
    
            let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
            let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
            let progress = CGFloat(positiveMovementOnAxisPercent)
            navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(1 - progress)
    
            switch sender.state {
            case .ended where sender.velocity(in: view).y >= velocityDismiss || progress > percentThresholdDismiss:
                // After animate, user made the conditions to leave
                UIView.animate(withDuration: 0.2, animations: {
                    switch self.axis {
                    case .vertical:
                        self.view.frame.origin.y = self.view.bounds.height
    
                    case .horizontal:
                        self.view.frame.origin.x = self.view.bounds.width
                    }
                    self.navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(0)
    
                }, completion: { finish in
                    self.dismiss(animated: true) //Perform dismiss
                })
            case .ended:
                // Revert animation
                UIView.animate(withDuration: 0.2, animations: {
                    switch self.axis {
                    case .vertical:
                        self.view.frame.origin.y = 0
    
                    case .horizontal:
                        self.view.frame.origin.x = 0
                    }
                })
            default:
                break
            }
            sender.setTranslation(.zero, in: view)
        }
    }
    

    【讨论】:

      【解决方案7】:

      大量更新 Swift 4 的 repo。

      对于 Swift 3,我创建了以下内容以从右到左呈现 UIViewController 并通过平移手势将其关闭。我已将其上传为GitHub repository

      DismissOnPanGesture.swift文件:

      //  Created by David Seek on 11/21/16.
      //  Copyright © 2016 David Seek. All rights reserved.
      
      import UIKit
      
      class DismissAnimator : NSObject {
      }
      
      extension DismissAnimator : UIViewControllerAnimatedTransitioning {
          func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
              return 0.6
          }
      
          func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
      
              let screenBounds = UIScreen.main.bounds
              let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
              let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
              var x:CGFloat      = toVC!.view.bounds.origin.x - screenBounds.width
              let y:CGFloat      = toVC!.view.bounds.origin.y
              let width:CGFloat  = toVC!.view.bounds.width
              let height:CGFloat = toVC!.view.bounds.height
              var frame:CGRect   = CGRect(x: x, y: y, width: width, height: height)
      
              toVC?.view.alpha = 0.2
      
              toVC?.view.frame = frame
              let containerView = transitionContext.containerView
      
              containerView.insertSubview(toVC!.view, belowSubview: fromVC!.view)
      
      
              let bottomLeftCorner = CGPoint(x: screenBounds.width, y: 0)
              let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
      
              UIView.animate(
                  withDuration: transitionDuration(using: transitionContext),
                  animations: {
                      fromVC!.view.frame = finalFrame
                      toVC?.view.alpha = 1
      
                      x = toVC!.view.bounds.origin.x
                      frame = CGRect(x: x, y: y, width: width, height: height)
      
                      toVC?.view.frame = frame
                  },
                  completion: { _ in
                      transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                  }
              )
          }
      }
      
      class Interactor: UIPercentDrivenInteractiveTransition {
          var hasStarted = false
          var shouldFinish = false
      }
      
      let transition: CATransition = CATransition()
      
      func presentVCRightToLeft(_ fromVC: UIViewController, _ toVC: UIViewController) {
          transition.duration = 0.5
          transition.type = kCATransitionPush
          transition.subtype = kCATransitionFromRight
          fromVC.view.window!.layer.add(transition, forKey: kCATransition)
          fromVC.present(toVC, animated: false, completion: nil)
      }
      
      func dismissVCLeftToRight(_ vc: UIViewController) {
          transition.duration = 0.5
          transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
          transition.type = kCATransitionPush
          transition.subtype = kCATransitionFromLeft
          vc.view.window!.layer.add(transition, forKey: nil)
          vc.dismiss(animated: false, completion: nil)
      }
      
      func instantiatePanGestureRecognizer(_ vc: UIViewController, _ selector: Selector) {
          var edgeRecognizer: UIScreenEdgePanGestureRecognizer!
          edgeRecognizer = UIScreenEdgePanGestureRecognizer(target: vc, action: selector)
          edgeRecognizer.edges = .left
          vc.view.addGestureRecognizer(edgeRecognizer)
      }
      
      func dismissVCOnPanGesture(_ vc: UIViewController, _ sender: UIScreenEdgePanGestureRecognizer, _ interactor: Interactor) {
          let percentThreshold:CGFloat = 0.3
          let translation = sender.translation(in: vc.view)
          let fingerMovement = translation.x / vc.view.bounds.width
          let rightMovement = fmaxf(Float(fingerMovement), 0.0)
          let rightMovementPercent = fminf(rightMovement, 1.0)
          let progress = CGFloat(rightMovementPercent)
      
          switch sender.state {
          case .began:
              interactor.hasStarted = true
              vc.dismiss(animated: true, completion: nil)
          case .changed:
              interactor.shouldFinish = progress > percentThreshold
              interactor.update(progress)
          case .cancelled:
              interactor.hasStarted = false
              interactor.cancel()
          case .ended:
              interactor.hasStarted = false
              interactor.shouldFinish
                  ? interactor.finish()
                  : interactor.cancel()
          default:
              break
          }
      }
      

      简单易用:

      import UIKit
      
      class VC1: UIViewController, UIViewControllerTransitioningDelegate {
      
          let interactor = Interactor()
      
          @IBAction func present(_ sender: Any) {
              let vc = self.storyboard?.instantiateViewController(withIdentifier: "VC2") as! VC2
              vc.transitioningDelegate = self
              vc.interactor = interactor
      
              presentVCRightToLeft(self, vc)
          }
      
          func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
              return DismissAnimator()
          }
      
          func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
              return interactor.hasStarted ? interactor : nil
          }
      }
      
      class VC2: UIViewController {
      
          var interactor:Interactor? = nil
      
          override func viewDidLoad() {
              super.viewDidLoad()
              instantiatePanGestureRecognizer(self, #selector(gesture))
          }
      
          @IBAction func dismiss(_ sender: Any) {
              dismissVCLeftToRight(self)
          }
      
          func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
              dismissVCOnPanGesture(self, sender, interactor!)
          }
      }
      

      【讨论】:

      • 太棒了!感谢分享。
      • 嗨,我如何使用 Pan Gesture Recognizer 进行演示,请帮助我,谢谢
      • 我现在正在创建一个带有教程的 youtube 频道。我可能会为 iOS 13+ / Swift 5 创建一集来介绍这个问题
      【解决方案8】:

      我想出了超级简单的方法来做到这一点。只需将以下代码放入您的视图控制器:

      斯威夫特 4

      override func viewDidLoad() {
          super.viewDidLoad()
          let gestureRecognizer = UIPanGestureRecognizer(target: self,
                                                         action: #selector(panGestureRecognizerHandler(_:)))
          view.addGestureRecognizer(gestureRecognizer)
      }
      
      @IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
          let touchPoint = sender.location(in: view?.window)
          var initialTouchPoint = CGPoint.zero
      
          switch sender.state {
          case .began:
              initialTouchPoint = touchPoint
          case .changed:
              if touchPoint.y > initialTouchPoint.y {
                  view.frame.origin.y = touchPoint.y - initialTouchPoint.y
              }
          case .ended, .cancelled:
              if touchPoint.y - initialTouchPoint.y > 200 {
                  dismiss(animated: true, completion: nil)
              } else {
                  UIView.animate(withDuration: 0.2, animations: {
                      self.view.frame = CGRect(x: 0,
                                               y: 0,
                                               width: self.view.frame.size.width,
                                               height: self.view.frame.size.height)
                  })
              }
          case .failed, .possible:
              break
          }
      }
      

      【讨论】:

      • 谢谢,完美运行!只需将 Pan Gesture Recogniser 拖放到界面构建器中的视图并与上述@IBAction 连接。
      • 也适用于 Swift 5。只需按照@balazs630 给出的说明进行操作即可。
      • 我认为这是最好的方法。
      • @Alex Shubin ,从 ViewController 拖到 TabbarController 时如何关闭?
      【解决方案9】:

      我创建了一个易于使用的扩展程序。

      只需将您的 UIViewController 与 InteractiveViewController 固有,您就完成了 InteractiveViewController

      从您的控制器调用方法 showInteractive() 以显示为交互式。

      【讨论】:

        【解决方案10】:

        Swift 4.x,使用 Pangesture

        简单的方法

        垂直

        class ViewConrtoller: UIViewController {
            override func viewDidLoad() {
                super.viewDidLoad()
                view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrage(_:))))
            }
        
            @objc func onDrage(_ sender:UIPanGestureRecognizer) {
                let percentThreshold:CGFloat = 0.3
                let translation = sender.translation(in: view)
        
                let newX = ensureRange(value: view.frame.minX + translation.x, minimum: 0, maximum: view.frame.maxX)
                let progress = progressAlongAxis(newX, view.bounds.width)
        
                view.frame.origin.x = newX //Move view to new position
        
                if sender.state == .ended {
                    let velocity = sender.velocity(in: view)
                   if velocity.x >= 300 || progress > percentThreshold {
                       self.dismiss(animated: true) //Perform dismiss
                   } else {
                       UIView.animate(withDuration: 0.2, animations: {
                           self.view.frame.origin.x = 0 // Revert animation
                       })
                  }
               }
        
               sender.setTranslation(.zero, in: view)
            }
        }
        

        辅助功能

        func progressAlongAxis(_ pointOnAxis: CGFloat, _ axisLength: CGFloat) -> CGFloat {
                let movementOnAxis = pointOnAxis / axisLength
                let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
                let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
                return CGFloat(positiveMovementOnAxisPercent)
            }
        
            func ensureRange<T>(value: T, minimum: T, maximum: T) -> T where T : Comparable {
                return min(max(value, minimum), maximum)
            }
        

        艰难的路

        参考这个 -> https://github.com/satishVekariya/DraggableViewController

        【讨论】:

        • 我尝试使用您的代码。但我想做一点改变,如果我的子视图位于底部,并且当用户拖动视图时,子视图的高度也应该相对于点击位置增加。注意:- 放置在子视图上的手势事件
        • 当然可以了
        • 嗨@SPatel关于如何修改此代码以用于在x轴左侧拖动的任何想法,即沿x轴的负运动?
        • @SPatel 您还需要更改答案的标题,因为垂直显示 x 轴移动,水平显示 y 轴移动。
        • 记得设置modalPresentationStyle = UIModalPresentationOverFullScreen,避免view后面出现后屏。
        【解决方案11】:

        创建了一个演示,用于交互式向下拖动以关闭视图控制器,如 snapchat 的发现模式。检查此github 以获取示例项目。

        【讨论】:

        • 很好,但它确实已经过时了。有人知道这样的另一个示例项目吗?
        【解决方案12】:

        这是我根据@Wilson 回答所做的扩展:

        // MARK: IMPORT STATEMENTS
        import UIKit
        
        // MARK: EXTENSION
        extension UIViewController {
        
            // MARK: IS SWIPABLE - FUNCTION
            func isSwipable() {
                let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
                self.view.addGestureRecognizer(panGestureRecognizer)
            }
        
            // MARK: HANDLE PAN GESTURE - FUNCTION
            @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
                let translation = panGesture.translation(in: view)
                let minX = view.frame.width * 0.135
                var originalPosition = CGPoint.zero
        
                if panGesture.state == .began {
                    originalPosition = view.center
                } else if panGesture.state == .changed {
                    view.frame.origin = CGPoint(x: translation.x, y: 0.0)
        
                    if panGesture.location(in: view).x > minX {
                        view.frame.origin = originalPosition
                    }
        
                    if view.frame.origin.x <= 0.0 {
                        view.frame.origin.x = 0.0
                    }
                } else if panGesture.state == .ended {
                    if view.frame.origin.x >= view.frame.width * 0.5 {
                        UIView.animate(withDuration: 0.2
                             , animations: {
                                self.view.frame.origin = CGPoint(
                                    x: self.view.frame.size.width,
                                    y: self.view.frame.origin.y
                                )
                        }, completion: { (isCompleted) in
                            if isCompleted {
                                self.dismiss(animated: false, completion: nil)
                            }
                        })
                    } else {
                        UIView.animate(withDuration: 0.2, animations: {
                            self.view.frame.origin = originalPosition
                        })
                    }
                }
            }
        
        }
        

        用法

        在您的视图控制器中,您希望可以滑动:

        override func viewDidLoad() {
            super.viewDidLoad()
        
            self.isSwipable()
        }
        

        作为导航控制器,从视图控制器的最左侧滑动即可将其关闭。

        【讨论】:

        • 嘿,我用过你的代码,它非常适合向右滑动,但我想在向下滑动时关闭,我该怎么做?请帮忙!
        【解决方案13】:

        仅垂直关闭

        func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
            let translation = panGesture.translation(in: view)
        
            if panGesture.state == .began {
                originalPosition = view.center
                currentPositionTouched = panGesture.location(in: view)    
            } else if panGesture.state == .changed {
                view.frame.origin = CGPoint(
                    x:  view.frame.origin.x,
                    y:  view.frame.origin.y + translation.y
                )
                panGesture.setTranslation(CGPoint.zero, in: self.view)
            } else if panGesture.state == .ended {
                let velocity = panGesture.velocity(in: view)
                if velocity.y >= 150 {
                    UIView.animate(withDuration: 0.2
                        , animations: {
                            self.view.frame.origin = CGPoint(
                                x: self.view.frame.origin.x,
                                y: self.view.frame.size.height
                            )
                    }, completion: { (isCompleted) in
                        if isCompleted {
                            self.dismiss(animated: false, completion: nil)
                        }
                    })
                } else {
                    UIView.animate(withDuration: 0.2, animations: {
                        self.view.center = self.originalPosition!
                    })
                }
            }
        

        【讨论】:

          【解决方案14】:

          在目标 C 中: 这是代码

          viewDidLoad

          UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc]
                                                       initWithTarget:self action:@selector(swipeDown:)];
          swipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
          [self.view addGestureRecognizer:swipeRecognizer];
          
          //Swipe Down Method
          
          - (void)swipeDown:(UIGestureRecognizer *)sender{
          [self dismissViewControllerAnimated:YES completion:nil];
          }
          

          【讨论】:

          • 如何控制在关闭前向下滑动的时间?
          【解决方案15】:

          您可以使用 UIPanGestureRecognizer 来检测用户的拖动并使用它移动模态视图。如果结束位置足够远,则可以关闭视图,或者以其他方式将其动画回其原始位置。

          查看this answer,了解有关如何实现此类功能的更多信息。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-07-06
            • 1970-01-01
            • 1970-01-01
            • 2019-11-12
            • 1970-01-01
            • 2022-01-06
            • 2020-06-11
            • 1970-01-01
            相关资源
            最近更新 更多