在iOS中,如何向下拖动以消除模态?


89

消除模态的一种常见方法是向下滑动-我们如何允许用户将模态向下拖动,如果距离足够远,则模态将被消除,否则动画返回原始位置?

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

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


看一下自定义的交互式过渡。这是实现它的方法。developer.apple.com/library/prerelease/ios/documentation/UIKit/…–
croX

罗伯特·陈(Robert Chen)引用了github.com/ThornTechPublic/InteractiveModal回购,并编写了一个wrapper / handler类来处理所有内容。没有更多的样板代码支持四种基本的转换(从上到下,从下到上,从左到右,从右到左)与解聘手势github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/...
Chamira费尔南多

@ChamiraFernando,查看了您的代码,它对您有很大帮助。有没有一种方法可以使多个方向而不是一个方向包含在内?
杰文·考威尔

我会去做的。这些天时间非常有限:(
Chamira Fernando

Answers:


93

我刚刚创建了一个教程,用于以交互方式下拉模式以将其关闭。

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

我发现这个主题起初很令人困惑,因此本教程逐步构建了该主题。

在此处输入图片说明

如果您只想自己运行代码,这是存储库:

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? {
        return DismissAnimator()
    }
    func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
    }
}

解雇动画师

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

import UIKit

class DismissAnimator : NSObject {
}

extension DismissAnimator : UIViewControllerAnimatedTransitioning {
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.6
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        guard
            let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
            let containerView = transitionContext.containerView()
            else {
                return
        }
        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) {
        dismissViewControllerAnimated(true, completion: nil)
    }

    @IBAction func handleGesture(sender: UIPanGestureRecognizer) {
        let percentThreshold:CGFloat = 0.3

        // convert y-position to downward pull progress (percentage)
        let translation = sender.translationInView(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 let interactor = interactor else { return }

        switch sender.state {
        case .Began:
            interactor.hasStarted = true
            dismissViewControllerAnimated(true, completion: nil)
        case .Changed:
            interactor.shouldFinish = progress > percentThreshold
            interactor.updateInteractiveTransition(progress)
        case .Cancelled:
            interactor.hasStarted = false
            interactor.cancelInteractiveTransition()
        case .Ended:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interactor.finishInteractiveTransition()
                : interactor.cancelInteractiveTransition()
        default:
            break
        }
    }

}

4
嘿罗伯特,辛苦了。您是否知道如何修改它以使其可用于表视图?也就是说,当表格视图位于顶部时,是否能够下拉以关闭?谢谢
Ross Barbish

1
罗斯,我创建了一个新的分支,其中有一个有效的示例:github.com/ThornTechPublic/InteractiveModal/tree/Ross。如果您想先看一下它的外观,请查看以下GIF:raw.githubusercontent.com/ThornTechPublic/InteractiveModal/…。表格视图具有内置的panGestureRecognizer,可以通过target-action将其连接到现有的handleGesture(_ :)方法。为了避免与正常的表滚动冲突,仅在将表滚动到顶部时才启动下拉菜单。我使用了快照,并添加了很多评论。
罗伯特·陈

罗伯特,做得更好。我确实做了自己的实现,该实现使用了现有的tableView平移方法,例如scrollViewDidScroll,scrollViewWillBeginDragging。它要求tableView的bounces和bouncesVertically都设置为true-这样我们就可以测量tableview项的ContentOffset。此方法的优点是,如果速度足够快(由于弹跳),似乎可以通过一个手势将表格视图从屏幕上刷下来。我可能会在本周的某个时间向您发送请求请求,这两个选项似乎都有效。
罗斯·巴比什

干得好@RossBarbish,我迫不及待想看看您是如何做到的。能够向上滚动然后进入交互式过渡模式,这都是一种流畅的运动,这将非常棒。
罗伯特·陈

1
设置presentation属性,segueover current context避免在拉下viewController时在背面出现黑屏
nitish005 2013年

62

我将分享我在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)以使其在达到一定速度时可拖动和禁用。

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!)
  }
  
  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
我复制了您的代码,它起作用了。模型视图的背景卜时的下拉是黑色的,而不是透明的,因为你
阮映越南

5
在故事板从属性检查器面板Storyboard segueMainViewControllerModalViewController:设置演示属性在现实语境
威尔逊

这似乎容易接受,但我在ViewControllerPannable类中遇到错误。错误为“无法调用非功能类型的UIPanGestureRecognizer的值”。在“ panGestureRecognizer = UIPanGestureRecognizer(target:self,action:#selector(panGestureAction(_ :)))”这一行上有什么想法吗?
tylerSF'4

通过将其更改为UIPanGestureRecognizer修复了我提到的错误。“ panGestureRecognizer = panGestureRecognizer(target ...”更改为:“ panGestureRecognizer = UIPanGestureRecognizer(target ...”
tylerSF

由于我显示的是VC,而不是模态显示的,因此在关闭时如何去除黑色背景?
Bean先生

17

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


先前解决方案的改进列表

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

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

您的代码中有错字或缺少变量“ minimumHeightRatioToHide”
shokaveli

1
感谢@shokaveli,已修复(是minimumScreenRatioToHide
agirault

这是一个非常好的解决方案。但是,我有一个小问题,不确定原因是什么:dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0红色背景是模式VC的一部分,蓝色背景是VC的一部分提出了模态。平移手势识别器启动时出现这种故障现象,我似乎无法解决。
Knolraap

1
嗨@Knolraap 也许在第一次self.view.frame.origin调用之前先看一下的值sliceViewVerticallyTo:似乎我们看到的偏移量与状态栏的高度相同,所以也许您的初始原点不为0?
agirault

1
最好在中slideViewVerticallyTo用作嵌套函数onPan
尼克·科夫,

16

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

在此处输入图片说明


2
很好,但是确实过时了。有人知道这样的另一个示例项目吗?
thelearner

14

Swift 4.x,使用手势

简单的方法

垂直

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

@SPatel,您好:关于如何修改此代码以用于在x轴左侧拖动的想法,即沿x轴的负向运动?
Nikhil Pandey

1
@SPatel您还需要更改答案的标题,因为垂直显示x轴移动,水平显示y轴移动。
Nikhil Pandey

1
记住要进行设置modalPresentationStyle = UIModalPresentationOverFullScreen,以免屏幕后面出现阴影view
tounaobun

13

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

斯威夫特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
    }
}

1
谢谢,完美的作品!只需将Pan Gesture Recogniser放到界面生成器中的视图中,然后与上面的@IBAction连接即可。
balazs630

也可以在Swift 5中使用。只需按照@ balazs630给出的说明进行操作即可。
LondonGuy

我认为这是最好的方法。
恩卡(Enkha)3:07

@Alex Shubin,从ViewController拖动到TabbarController时如何解散?
Vadlapalli Masthan

11

大量更新Swift 4的仓库

对于Swift 3,我创建了以下内容以UIViewController从右到左显示一个并通过平移手势将其关闭。我已经将其上传为GitHub存储库

在此处输入图片说明

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

1
太棒了!感谢分享。
莎拉德·乔汉

嗨,我该如何使用Pan Gesture Recognizer呈现,谢谢您的帮助
Muralikrishna

我现在正在使用教程启动YouTube频道。我可能会创建一集内容来解决此问题,适用于iOS 13 + / Swift 5
David Seek

6

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

我的书解释了如何执行此操作,并且已经发布了示例(摘自本书)。这个特定示例的情况有所不同-过渡是横向的,不是向下的,它是针对选项卡栏控制器而不是所提供的控制器的-但基本思想是完全相同的:

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

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


3
404页面不存在。
捕手

6

仅纵向解雇

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

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

只是固有的UIViewController和InteractiveViewController,您就完成了 InteractiveViewController

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

在此处输入图片说明


4

在目标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];
}

如何控制取消之前的向下滑动时间?
TonyTony

2

这是我根据@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()
}

并且可以通过从视图控制器的最左侧滑动(作为导航控制器)将其关闭。


嘿,我已经使用了您的代码,它可以完美地向右滑动,但是我想在向下滑动时关闭,我该怎么做?请帮忙!

2

这是我从axis拖动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)
    }
}

2

对于那些真的想深入了解Custom UIViewController Transition的人,我推荐raywenderlich.com上的这篇很棒的教程

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

这里是预览:

而且它也是交互式的!

骇客骇客!


0

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

请查看此答案,以获取有关如何实现类似内容的更多信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.