在iOS中,如何向下拖动以关闭模态窗口?

107

一种常见的关闭模态框的方式是向下滑动-如何允许用户拖动模态框向下,如果足够远,模态框将关闭,否则它会以原始位置动画返回?

例如,在Twitter应用程序的照片视图或Snapchat的“发现”模式中可以找到这个使用。

类似的帖子指出,我们可以使用UISwipeGestureRecognizer和[self dismissViewControllerAnimated ...]来在用户向下滑动时关闭模态VC。但这只处理单个滑动,不允许用户拖动模态框。


1
看一下自定义交互式转换。这是你可以实现它的方式。https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewControllerInteractiveTransitioning_protocol/index.html - croX
1
参考了Robert Chen的https://github.com/ThornTechPublic/InteractiveModal仓库,并编写了一个包装器/处理类来处理所有内容。不再需要样板代码,支持四种基本过渡(从上到下,从下到上,从左到右和从右到左)以及解除手势 https://github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/UIViewControllerBasicTransitioningHandler.swift - Chamira Fernando
@ChamiraFernando,我看了你的代码,它帮了我很多。有没有办法让它包含多个方向而不是一个? - Jevon Cowell
我会做的。时间这些天是一个巨大的限制 :( - Chamira Fernando
https://github.com/satishVekariya/DraggableViewController - SPatel
它对我起作用了:https://github.com/ModernProgrammer/DragDismissDemo - Ravi
16个回答

101

我刚刚创建了一个教程,讲解如何交互式地拖动模态框以将其关闭。

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

一开始我觉得这个主题很令人困惑,所以本教程会逐步构建。

enter image description here

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

https://github.com/ThornTechPublic/InteractiveModal

这是我所采用的方法:
视图控制器
您可以使用自定义动画覆盖dismiss动画。如果用户在拖动模态框时,交互器会介入。
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 以使其充当状态机。由于交互器对象被两个视图控制器访问,因此您可以使用它来跟踪滑动进度。

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
       }
    }
    
}

4
嘿,罗伯特,干得好。你有没有想过如何修改它以使其能够与表视图一起使用?也就是说,在表格视图在顶部时,能够向下拉动以关闭它?谢谢。 - Ross Barbish
2
Ross,我创建了一个新的分支,其中包含一个工作示例:https://github.com/ThornTechPublic/InteractiveModal/tree/Ross。如果您想先看看它的样子,请查看此GIF:https://raw.githubusercontent.com/ThornTechPublic/InteractiveModal/master/tableviewDismiss.gif。表视图具有内置的panGestureRecognizer,可以通过目标操作将其连接到现有的handleGesture(_ :)方法。为了避免与正常的表滚动冲突,只有当表滚动到顶部时,下拉关闭才会启动。我使用了快照,并添加了很多注释。 - Robert Chen
Robert,干得好。我自己创造了一种使用现有的tableView滚动方法(如scrollViewDidScroll、scrollViewWillBeginDragging)的实现方式。它要求tableView必须设置bounces和bouncesVertically为true,这样我们就可以测量tableview项的ContentOffset。这种方法的优点是,如果有足够的速度(由于弹跳),它似乎允许将tableview在一个手势中从屏幕上滑动出去。这个星期我可能会给你发送一个pull request,两种选项都似乎有效。 - Ross Barbish
1
segue的演示属性设置为over current context,以避免在下拉视图控制器时出现黑屏。 - nitish005
@RobertChen,感谢您的出色回答。您能展示一下如何解决滚动视图中的冲突吗(Swift 4 中表视图的解决方案不适用于滚动视图)? - Tigran Iskandaryan
显示剩余5条评论

71
我将分享我如何在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
  }
}

模态视图控制器继承自我构建的classViewControllerPannable),以使它们在达到一定速度时可拖动和可解除。

ViewControllerPannable类

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!
        })
      }
    }
  }
}

1
我复制了你的代码,它可以工作。但是当下拉时,模型视图的背景是黑色的,而不像你的那样透明。 - Nguyễn Anh Việt
5
MainViewControllerModalViewControllerStoryboard segueAttributes Inspector面板中:将Presentation属性设置为Over Current Context - Wilson
这似乎比被接受的答案更容易,但我在 ViewControllerPannable 类中遇到了一个错误。错误是“不能调用非函数类型 UIPanGestureRecognizer 的值”。它在“panGestureRecognizer = UIPanGestureRecognizer(target:self,action:#selector(panGestureAction(_ :)))”一行上。有什么想法吗? - tylerSF
通过将其更改为UIPanGestureRecognizer,修复了我提到的错误。 "panGestureRecognizer = panGestureRecognizer(target ..." 更改为:"panGestureRecognizer = UIPanGestureRecognizer(target ..." - tylerSF
由于我是以非模态方式呈现VC,那么在解除时如何去除黑色背景? - Mr. Bean
显示剩余2条评论

26

这是一个基于@wilson答案的单文件解决方案(感谢),具有以下改进:


改进列表

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

解决方案

Swift 3和Swift 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;
    }
}

1
谢谢@shokaveli,已修复(原为minimumScreenRatioToHide)。 - agirault
这是一个非常好的解决方案。然而,我有一个小问题,不太确定原因是什么: https://www.dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 红色背景是模态视图控制器的一部分,蓝色背景是呈现模态的视图控制器的一部分。当平移手势识别器开始时会出现一些故障行为,我似乎无法解决。 - Knolraap
1
嗨@Knolraap。在第一次调用sliceViewVerticallyTo之前,也许可以查看self.view.frame.origin的值:因为我们看到的偏移量与状态栏高度相同,所以也许你的初始原点不是0? - agirault
1
哇,这太棒了 :) 只是一个问题,我正在展示我的视图控制器嵌入在一个UINavgiationController中,有没有可能让导航控制器可关闭?目前,导航栏停留在顶部,并且在下拉VC时有一个黑屏幕。任何指针都将不胜感激。谢谢 :) - Munib
1
最好将slideViewVerticallyTo作为onPan中的嵌套函数使用。 - Nike Kov
显示剩余3条评论

22

我找到了一种非常简单的方法来做到这一点。只需将以下代码放入您的视图控制器:

Swift 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
    }
}

2
谢谢,完美地运作了!只需在界面构建器中向视图添加一个Pan手势识别器,并将其连接到上述@IBAction即可。 - balazs630
也适用于Swift 5。只需按照@balazs630的说明操作即可。 - LondonGuy
我认为这是最好的方式。 - Enkha
@Alex Shubin,如何在从ViewController拖动到TabbarController时解除? - Vadlapalli Masthan
1
我已经在方法之外声明了“var initialTouchPoint = CGPoint.zero”,对于我的需求,它表现更佳。在我的修改之前,如果您从屏幕中间开始划动滑动,动画将从该点跳转/启动,从我的角度来看,这看起来不好,而在我的修改后,它将有效地使用拖动差异将视图控制器动画到底部。 - elvisrusu

18

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


1
我尝试使用您的代码,但我想做一个小改变,我的子视图在底部,当用户拖动视图时,子视图的高度也应随着点击位置的变化而增加。注意:手势事件被放置在子视图上。 - Ekra
当然,你可以做到。 - SPatel
1
@SPatel,您还需要更改答案的标题,因为垂直方向显示x轴运动,而水平方向显示y轴运动。 - Nikhil Pandey
1
请记得设置 modalPresentationStyle = UIModalPresentationOverFullScreen 以避免在 view 后面出现黑屏。 - tounaobun
@spnkr 不错 - SPatel
显示剩余7条评论

16

创建了一个演示互动拖动关闭视图控制器的demo,类似于Snapchat的Discover模式。查看GitHub上的样例项目。

在此输入图像描述


2
很好,但它真的过时了。有人知道类似这样的另一个示例项目吗? - thelearner

12

大规模更新 Swift 4 的仓库。

对于 Swift 3,我创建了以下内容,以从右到左呈现 UIViewController 并通过手势滑动将其解除。我已将此上传为 GitHub 仓库

enter image description here

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)
    }
    
    @objc func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
        dismissVCOnPanGesture(self, sender, interactor!)
    }
}

1
真是太棒了!感谢分享。 - Sharad Chauhan
嗨,我该如何使用Pan手势识别器进行演示,请帮帮我,谢谢。 - Muralikrishna
我现在正在开始一个带有教程的YouTube频道。我可能会创建一集来解决iOS 13+ / Swift 5的问题。 - David Seek

7

仅垂直方向取消

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!
            })
        }
    }

6
你所描述的是一种交互式自定义转场动画。你正在定制转场的动画和驱动手势,即呈现视图控制器的消失(或非消失)。最简单的实现方法是将UIPanGestureRecognizer与UIPercentDrivenInteractiveTransition结合使用。
我的书中讲解了如何实现这一点,并且我已经发布了实例(来自本书)。这个特定的例子是一个不同的情况——转场是侧向的,而不是向下的,并且它是针对选项卡栏控制器,而不是呈现的控制器——但基本思路完全相同:

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

如果您下载该项目并运行它,您会发现正在发生的事情与您所描述的完全相同,只是侧面:如果拖动超过一半,我们进行转换,否则我们取消并回到原位。

3
404 页面未找到。 - trapper

6
我已经创建了一个易于使用的扩展程序。
只需将您的UIViewController继承InteractiveViewController,然后您就完成了 InteractiveViewController 从您的控制器调用showInteractive()方法以显示为交互式。 enter image description here

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接