В iOS, как перетащить, чтобы отклонить модальный?
Общим способом отклонения модальности является прокрутка вниз. Как мы разрешаем пользователю перетаскивать модальный вниз, если он достаточно далеко, модальный уволен, в противном случае он возвращается в исходное положение?
Например, мы можем найти это, используемое при просмотре фото в приложении Twitter, или в режиме Snapchat "открыть".
Аналогичные темы указывают, что мы можем использовать UISwipeGestureRecognizer и [self rejectViewControllerAnimated...], чтобы отклонить модальный VC, когда пользователь отскакивает. Но это только обрабатывает один салфетки, не позволяя пользователю перетаскивать модальный объект.
Ответы
Ответ 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()
}
)
}
}