В iOS как перетащить вниз, чтобы закрыть модальное окно?

91

Обычный способ отклонить модальное окно - провести пальцем вниз. Как мы можем позволить пользователю перетащить модальное окно вниз, если оно достаточно далеко, модальное окно закрывается, иначе оно возвращается в исходное положение?

Например, мы можем найти это используемым в просмотрах фотографий в приложении Twitter или в режиме «обнаружения» Snapchat.

Подобные потоки указывают на то, что мы можем использовать UISwipeGestureRecognizer и [self dismissViewControllerAnimated ...], чтобы закрыть модальный VC, когда пользователь проведет пальцем вниз. Но это обрабатывает только одно движение, не позволяя пользователю перетаскивать модальное окно.

foobar
источник
Взгляните на настраиваемые интерактивные переходы. Вот как вы можете это реализовать. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX
Роберт Чен обратился к репозиторию github.com/ThornTechPublic/InteractiveModal и написал класс оболочки / обработчика для обработки всего. Больше никакой шаблонный код не поддерживает четыре основных перехода (сверху вниз, снизу вверх, слева направо и справа налево) с отклоняющими жестами github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/…
Chamira Fernando
@ChamiraFernando, посмотрел ваш код, и это очень помогает. Есть ли способ сделать так, чтобы было включено несколько направлений вместо одного?
Джевон Коуэлл
Я сделаю. В наши дни время очень ограничено :(
Chamira Fernando

Ответы:

95

Я только что создал учебник для интерактивного перетаскивания модального окна, чтобы его закрыть.

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

Закрыть Animator

Вы создаете собственный аниматор. Это настраиваемая анимация, которую вы упаковываете в 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())
            }
        )
    }
}

Interactor

Вы создаете подкласс, 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) {
        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
Привет, Роберт, отличная работа. У вас есть идея, как мы могли бы изменить это, чтобы оно могло работать с табличными представлениями? То есть возможность опустить вниз, чтобы закрыть, когда табличное представление находится наверху? Спасибо
Росс Барбиш
1
Росс, я создал новую ветку с рабочим примером: github.com/ThornTechPublic/InteractiveModal/tree/Ross . Если вы хотите сначала увидеть, как это выглядит, посмотрите этот GIF: raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . Табличное представление имеет встроенный panGestureRecognizer, который можно связать с существующим методом handleGesture (_ :) через target-action. Чтобы избежать конфликта с обычной прокруткой таблицы, закрытие раскрывающегося списка срабатывает только тогда, когда таблица прокручивается вверх. Я использовал снимок и добавил много комментариев.
Роберт Чен
Роберт, еще больше отличной работы. Я сделал свою собственную реализацию, которая использует существующие методы панорамирования tableView, такие как scrollViewDidScroll, scrollViewWillBeginDragging. Он требует, чтобы для tableView было установлено значение bounces и bouncesVertical как true - таким образом мы можем измерить ContentOffset элементов tableview. Преимущество этого метода заключается в том, что он позволяет убрать табличное представление с экрана одним движением, если скорость достаточно высока (из-за подпрыгивания). Я, вероятно, отправлю вам запрос на перенос где-то на этой неделе, оба варианта кажутся допустимыми.
Росс Барбиш
Хорошая работа, @RossBarbish, не могу дождаться, чтобы увидеть, как тебе это удалось. Будет довольно приятно иметь возможность прокручивать вверх, а затем переходить в интерактивный режим перехода одним плавным движением.
Роберт Чен
1
установите свойство презентации segueна, over current contextчтобы избежать черного экрана сзади, когда вы опускаете viewController
nitish005 09
63

Поделюсь, как я это сделал в 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 segueиз MainViewControllerк ModalViewController: установите Представление собственности Over Current Context
Уилсон
Это кажется проще, чем принятый ответ, но я получаю сообщение об ошибке в классе ViewControllerPannable. Ошибка: «Невозможно вызвать значение нефункционального типа UIPanGestureRecognizer». Он находится в строке «panGestureRecognizer = UIPanGestureRecognizer (target: self, action: #selector (panGestureAction (_ :)))» Есть идеи?
tylerSF
Исправлена ​​ошибка, о которой я упоминал, изменив ее на UIPanGestureRecognizer. "panGestureRecognizer = panGestureRecognizer (цель ..." изменена на: "panGestureRecognizer = UIPanGestureRecognizer (цель ..."
tylerSF 02
Поскольку я представляю ВК, а не представляю ее модально, как я могу удалить черный фон при увольнении?
Мистер Бин,
20

Вот однофайловое решение, основанное на ответе @wilson (спасибо 👍) со следующими улучшениями:


Список улучшений из предыдущего решения

  • Ограничьте панорамирование так, чтобы вид уменьшался:
    • Избегайте горизонтального перевода, обновляя только y координатыview.frame.origin
    • Избегайте панорамирования экрана при смахивании вверх с помощью let y = max(0, translation.y)
  • Также отключите контроллер представления в зависимости от того, где отпущен палец (по умолчанию в нижней половине экрана), а не только на основе скорости смахивания
  • Показывать контроллер представления как модальный, чтобы предыдущий контроллер представления отображался позади и избегал черного фона (должен ответить на ваш вопрос @ nguyễn-anh-việt)
  • Удалите ненужные currentPositionTouchedиoriginalPosition
  • Выставьте следующие параметры:
    • 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;
    }
}
Agirault
источник
В вашем коде либо опечатка, либо отсутствует переменная "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

создал демонстрацию для интерактивного перетаскивания вниз, чтобы отключить контроллер представления, например режим обнаружения в Snapchat. Проверьте этот github для примера проекта.

введите описание изображения здесь

med
источник
2
Отлично, но это действительно устарело. Кто-нибудь знает другой подобный пример проекта?
thelearner
14

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 Есть идеи, как изменить этот код, чтобы использовать его для перетаскивания в левой части оси x, т.е. отрицательных движений по оси x?
Nikhil Pandey
1
@SPatel Вам также необходимо изменить заголовок ответа, так как по вертикали отображаются движения по оси x, а по горизонтали - по оси Y.
Nikhil Pandey
1
Не забудьте установить, modalPresentationStyle = UIModalPresentationOverFullScreenчтобы задний экран не находился за view.
tounaobun
13

Я придумал очень простой способ сделать это. Просто поместите следующий код в свой контроллер представления:

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
    }
}
Алексей Шубин
источник
1
Спасибо, работает отлично! Просто перетащите средство распознавания жестов панорамирования в представление в построителе интерфейса и подключитесь к указанному выше @IBAction.
balazs630
Также работает в Swift 5. Просто следуйте инструкциям, которые дал @ balazs630.
LondonGuy
Думаю, это лучший способ.
Энха,
@Alex Shubin, как уволить при перетаскивании из ViewController в TabbarController?
Вадлапалли Мастхан,
12

Массово обновляет репозиторий 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, пожалуйста, помогите мне, спасибо
Мураликришна
Прямо сейчас я запускаю канал на YouTube с обучающими материалами. Я мог бы создать эпизод, посвященный этой проблеме, для iOS 13+ / Swift 5
Дэвид Сик
6

Вы описываете интерактивную настраиваемую анимацию перехода . Вы настраиваете как анимацию, так и движущий жест перехода, то есть увольнение (или нет) представленного контроллера представления. Самый простой способ реализовать это - объединить UIPanGestureRecognizer с UIPercentDrivenInteractiveTransition.

Моя книга объясняет, как это сделать, и я опубликовал примеры (из книги). Этот конкретный пример представляет собой другую ситуацию - переход сбоку, а не вниз, и он предназначен для контроллера панели вкладок, а не для представленного контроллера - но основная идея точно такая же:

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

Если вы загрузите этот проект и запустите его, вы увидите, что происходит именно то, что вы описываете, за исключением того, что оно происходит в сторону: если перетаскивание больше половины, мы переходим, но если нет, мы отменяем и возвращаемся в место.

матовый
источник
3
404 Страница не найдена.
trapper
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!
            })
        }
    }
мисс Gbot
источник
6

Я создал простое в использовании расширение.

Просто присуще вашему UIViewController с InteractiveViewController, и вы сделали InteractiveViewController

вызовите метод showInteractive () из вашего контроллера, чтобы он отображался как интерактивный.

введите описание изображения здесь

Шоаиб
источник
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()
}

и его можно отключить, проведя пальцем по экрану с крайней левой стороны контроллера представления в качестве контроллера навигации.

Fredericdnd
источник
Привет, я использовал ваш код, и он отлично работает для смахивания вправо, но я хочу отклонить смахивание вниз, как я могу это сделать? Пожалуйста помоги!
Khush
2

Это мой простой класс для перетаскивания 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 для обнаружения перетаскивания пользователем и перемещения модального представления вместе с ним. Если конечная позиция находится достаточно далеко вниз, вид можно закрыть или иным образом анимировать обратно в исходное положение.

Ознакомьтесь с этим ответом для получения дополнительной информации о том, как реализовать что-то подобное.

DanielG
источник