Подтвердить что ты не робот

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

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

Например, мы можем найти это, используемое при просмотре фото в приложении Twitter, или в режиме Snapchat "открыть".

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

4b9b3361

Ответ 1

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

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? {
        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())
            }
        )
    }
}

Interactor

Подкласс 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
        }
    }

}

Ответ 2

Я расскажу, как я это сделал в 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
  }
}

Если контроллеры вида Modals наследуют от class, который я создал (ViewControllerPannable), чтобы сделать их перетаскиваемыми и недоступными при достижении определенной скорости.

ViewControllerPannable class

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

Ответ 3

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

enter image description here

Ответ 4

Массово обновляет репо для 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)
    }

    func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
        dismissVCOnPanGesture(self, sender, interactor!)
    }
}

Ответ 5

Вот однофайловое решение, основанное на ответе @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 с)

Решение

Свифт 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(_:)))
        self.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)
            self.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
                        self.dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                // If not closing, reset the view to the top
                UIView.animate(withDuration: animationDuration, animations: {
                    self.slideViewVerticallyTo(0)
                })
            }

        default:
            // If gesture state is undefined, reset the view to the top
            UIView.animate(withDuration: animationDuration, animations: {
                self.slideViewVerticallyTo(0)
            })

        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overFullScreen;
        self.modalTransitionStyle = .coverVertical;
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.modalPresentationStyle = .overFullScreen;
        self.modalTransitionStyle = .coverVertical;
    }
}

Ответ 6

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

Ответ 7

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

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

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

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

Ответ 8

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

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

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

enter image description here

Ответ 9

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

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

Ответ 10

Только вертикальное отклонение

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

Ответ 11

В Цель 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];
}

Ответ 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
                })
            }
        }
    }

}

ИСПОЛЬЗОВАНИЕ

Внутри вашего контроллера просмотра вы хотите быть swipable:

override func viewDidLoad() {
    super.viewDidLoad()

    self.isSwipable()
}

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

Ответ 13

Это мой простой класс для Drag 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)
    }
}

Ответ 14

Вы можете использовать UIPanGestureRecognizer для обнаружения пользовательского перетаскивания и перемещения модального представления с ним. Если конечная позиция достаточно далека, представление может быть отклонено или иным образом перенесено в исходное положение.

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

Ответ 15

import AVFoundation
import UIKit





protocol PlayerEventDelegate : class{
    func didUpdateTimer(_ player : AVPlayer, elpsed time : String)
    func totalTime(_ player : AVPlayer)
    func AVPlayer(didPause player : AVPlayer)
    func AVPlayer(didPlay player : AVPlayer)
}
extension PlayerEventDelegate {
    func didUpdateTimer(_ player : AVPlayer, elpsed time : String){}
    func AVPlayer(didPause player : AVPlayer){}
    func AVPlayer(didPlay player : AVPlayer){}
}

class ViewVideo : UIView
{
    var playerLayer: AVPlayerLayer?
    var player: AVPlayer?
    var isLoop: Bool = false
    open weak var delegate : PlayerEventDelegate?
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

    }

    func configure(url: String) {

        if let videoURL = URL(string: url) {
            player = AVPlayer(url: videoURL)
            playerLayer = AVPlayerLayer(player: player)
            playerLayer?.frame = bounds
            playerLayer?.videoGravity = AVLayerVideoGravity.resize
            if let playerLayer = self.playerLayer {
                layer.addSublayer(playerLayer)
            }
            NotificationCenter.default.addObserver(self, selector: #selector(reachTheEndOfTheVideo), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem)

            player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main, using: { [weak self] (time) in
                guard let strongSelf = self else{
                    return
                }

                if strongSelf.player!.currentItem?.status == .readyToPlay {
                    let currentTime = CMTimeGetSeconds(strongSelf.player!.currentTime())

                    let secs = Int(currentTime)
                    let timetext = NSString(format: "%02d:%02d", secs/60, secs%60) as String//"\(secs/60):\(secs%60)"
                    strongSelf.delegate?.didUpdateTimer(strongSelf.player!, elpsed: timetext)
                }
            })

        }
    }

    func play() {
        if player?.timeControlStatus != AVPlayer.TimeControlStatus.playing {
            player?.play()
            self.delegate?.totalTime(self.player!)
            self.delegate?.AVPlayer(didPlay: self.player!)
        }
    }
    var isPlaying : Bool{
        return player?.timeControlStatus == AVPlayer.TimeControlStatus.playing
    }

    func pause() {
        player?.pause()
        self.delegate?.AVPlayer(didPause: self.player!)
    }

    func stop() {
        player?.pause()
        player?.seek(to: CMTime.zero)

    }

    @objc func reachTheEndOfTheVideo(_ notification: Notification) {
        if isLoop {
            player?.pause()
            player?.seek(to: CMTime.zero)
            player?.play()
        }
    }
    deinit {
//        self.player?.removeTimeObserver(self)
    }


}


import UIKit
import AVKit

class ViewController: UIViewController {

    @IBOutlet weak var btnPlay: UIButton!
    @IBOutlet weak var lblTotalTime: UILabel!
    @IBOutlet weak var lblStartTime: UILabel!
    @IBOutlet weak var sliderProgress: UISlider!
    @IBOutlet weak var btnExpand: UIButton!
    @IBOutlet weak var viewOverlay: UIView!
    @IBOutlet weak var viewVideo: ViewVideo!
    var totalTime : Double = 0
    var timer : Timer?
    var interactor:Interactor? = nil
    var swipe : UIPanGestureRecognizer?
    var isToolHidden = true
    override func viewDidLoad() {
        super.viewDidLoad()
        //        self.viewOverlay.isHidden = true
        viewVideo.configure(url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4")
        viewVideo.isLoop = true
        viewVideo.delegate = self
        let gestureControll = UITapGestureRecognizer(target: self, action: #selector(didTouchOverlay))
        gestureControll.numberOfTapsRequired = 1
        self.viewVideo.addGestureRecognizer(gestureControll)
        updateFocusVideo()
        self.swipe = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
        swipe?.maximumNumberOfTouches = 1
        self.view.addGestureRecognizer(swipe!)
        NotificationCenter.default.addObserver(self, selector: #selector(didChangeOrientation), name: UIDevice.orientationDidChangeNotification, object: nil)
        // Do any additional setup after loading the view.
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.viewVideo.playerLayer?.frame = self.viewVideo.bounds
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        AppUtility.lockOrientation(.all)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.viewVideo.pause()
        let value = UIInterfaceOrientation.portrait.rawValue
        UIDevice.current.setValue(value, forKey: "orientation")
        AppUtility.lockOrientation(.portrait)

    }

    @IBAction func sliderDidChangeValue(_ sender: Any) {
        self.timer?.invalidate()
        self.timer = nil
        let value = self.sliderProgress.value
        let durationToSeek = Float(self.totalTime) * value
        if let player = self.viewVideo.player{
            player.seek(to: CMTimeMakeWithSeconds(Float64(durationToSeek),preferredTimescale: player.currentItem!.duration.timescale)) { [weak self](state) in
                guard let strongSelf = self else{return}
                strongSelf.updateFocusVideo()
            }
        }
    }

    @IBAction func btnExpandTouched(_ sender: Any) {
        if UIDeviceOrientation.portrait == UIDevice.current.orientation
        {
            let value = UIInterfaceOrientation.landscapeLeft.rawValue
            UIDevice.current.setValue(value, forKey: "orientation")
        }
        else{
            let value = UIInterfaceOrientation.portrait.rawValue
            UIDevice.current.setValue(value, forKey: "orientation")
        }
    }

    @IBAction func btnPlayTouched(_ sender: Any) {
        if self.viewVideo.isPlaying
        {
            self.timer?.invalidate()
            self.timer = nil
            self.viewVideo.pause()
            self.isToolHidden = false
        }
        else{
            self.viewVideo.play()
            self.viewOverlay.isHidden = true
            self.isToolHidden = true
        }
    }


    @objc func didChangeOrientation(gesture : Notification)
    {
        if UIDevice.current.orientation == .portrait
        {
            self.swipe?.addTarget(self, action: #selector(handleGesture))
        }
        else{
            self.swipe?.removeTarget(self, action: nil)
        }
    }
    @objc func didSwipeGesture(gesture : UIGestureRecognizer)
    {
        //        let transition = CATransition()
        //        transition.type = .fade
        //        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        //        transition.subtype = .fromTop
        //        transition.duration = 0.5
        //        self.view.layer.add(transition, forKey: "fade")
        //        self.dismiss(animated: false, completion: nil)
        //
    }

    @objc func didTouchOverlay()
    {
        if self.isToolHidden
        {
            DispatchQueue.main.async {
                self.viewOverlay.isHidden = false
                self.timer = nil
                self.updateFocusVideo()
                self.isToolHidden = false
            }
        }
        else{
            self.viewOverlay.isHidden = true
            self.isToolHidden = true
        }
    }

    func AdjustToolBar()
    {
        if self.isToolHidden == false
        {
            self.viewOverlay.isHidden = true
            self.isToolHidden = true
        }
    }

    func updateFocusVideo() {
        if timer == nil{
            self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {[weak self] (time) in
                guard let strongSelf = self else{return}
                strongSelf.AdjustToolBar()
            })
        }
        else{
            timer?.invalidate()
            timer = nil

        }
    }

}
extension ViewController : PlayerEventDelegate
{
    func didUpdateTimer(_ player: AVPlayer, elpsed time: String) {
        self.lblStartTime.text = time
        let currentTime:Double = player.currentItem?.currentTime().seconds ?? 0
        let currentItem = player.currentItem
        let duration = currentItem?.asset.duration
        self.sliderProgress.setValue(Float(currentTime / duration!.seconds) , animated: true)
    }

    func totalTime(_ player: AVPlayer) {
        let currentItem = player.currentItem
        let duration = currentItem?.asset.duration
        let sec = CMTimeGetSeconds(duration!)
        self.totalTime = duration?.seconds ?? 0
        self.setHoursMinutesSecondsFrom(seconds: sec)

    }

    func AVPlayer(didPause player: AVPlayer) {
        self.btnPlay.setTitle("Play", for: .normal)
    }

    func AVPlayer(didPlay player: AVPlayer) {
        self.btnPlay.setTitle("Pause", for: .normal)
    }

    func setHoursMinutesSecondsFrom(seconds: Double){
        let secs = Int(seconds)

        self.lblTotalTime.text = NSString(format: "%02d:%02d", secs/60, secs%60) as String
    }
}


extension ViewController {

    @objc func handleGesture(_ sender: UIPanGestureRecognizer) {

        let percentThreshold:CGFloat = 0.3

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

        switch sender.state {
        case .began:
            interactor.hasStarted = true
            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
        }
    }

    func showHelperCircle(){
        let center = CGPoint(x: view.bounds.width * 0.5, y: 100)
        let small = CGSize(width: 30, height: 30)
        let circle = UIView(frame: CGRect(origin: center, size: small))
        circle.layer.cornerRadius = circle.frame.width/2
        circle.backgroundColor = UIColor.white
        circle.layer.shadowOpacity = 0.8
        circle.layer.shadowOffset = CGSize()
        view.addSubview(circle)
        UIView.animate(
            withDuration: 0.5,
            delay: 0.25,
            options: [],
            animations: {
                circle.frame.origin.y += 200
                circle.layer.opacity = 0
        },
            completion: { _ in
                circle.removeFromSuperview()
        }
        )
    }
}