In iOS, how to drag to reject modal?

A common way to reject modality is to scroll down. How do we allow the user to drag the modal down if it is far enough, the modal is fired, otherwise it will return to its original position?

For example, we can find this, used when viewing photos in the Twitter application, or in Snapchat "open" mode.

Similar topics indicate that we can use UISwipeGestureRecognizer and [self rejectViewControllerAnimated ...] to reject the modal VC when the user bounces. But this only handles one swipe, not allowing the user to drag the modal object.

+73
ios gesture-recognition viewcontroller
Mar 26 '15 at 22:52
source share
15 answers

I just created a tutorial for interactively dragging a modal to fire it.

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

At first, I realized that this topic was misleading, so the tutorial creates it step by step.

enter image description here

If you just want to run the code yourself, this will be repo:

https://github.com/ThornTechPublic/InteractiveModal

This is the approach I used:

View controller

You override the dismissal animation using custom. If the user drags the modal code, interactor starts.

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

Reject animator

A custom animator is created. This is a custom animation that you pack inside the UIViewControllerAnimatedTransitioning protocol.

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

Interactractor

Subclass UIPercentDrivenInteractiveTransition so that it can act as your state machine. Because the proxy accesses both VCs, use it to track pan progress.

 import UIKit class Interactor: UIPercentDrivenInteractiveTransition { var hasStarted = false var shouldFinish = false } 

Modal view controller

This displays the state of the panorama gestures on the interaction method calls. The value translationInView() y determines whether the user has crossed the threshold. When the panorama gesture is. .Ended , the interactor either finishes or cancels.

 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 } } } 
+85
Feb 04 '16 at 15:55
source share

I will tell you how I did it in Swift 3:

Result

Implementation

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

If view controllers of Modals inherit from the class that I created ( ViewControllerPannable ) to make them draggable and inaccessible when they reach a certain speed.

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! }) } } } } 
+54
Dec 10 '16 at 15:20
source share

created a demo version for interactive drag and drop to close the view manager, such as snapchat mode. Check out this github for a sample project.

enter image description here

+15
Apr 26 '16 at 11:26
source share

Bulk updates repo for Swift 4.

For Swift 3, I created the following to represent the UIViewController from right to left and reject it with a pan gesture. I downloaded it as a GitHub repository .

enter image description here

DismissOnPanGesture.swift file:

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

Simple use:

 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!) } } 
+9
Nov 21 '16 at 16:34
source share

Here is a single-file solution based on @wilson's answer (thanks ๐Ÿ‘) with the following improvements:




List of improvements from previous solution

  • Limit panning so that the view only gets worse:
    • Avoid horizontal translation by only updating the y coordinate of view.frame.origin
    • Avoid panning off the screen when swiping with let y = max(0, translation.y)
  • Also reject the view controller based on where the finger is released (by default in the lower half of the screen), and not just based on the scrolling speed
  • Show the view controller as modal to ensure that the previous view controller is behind and to avoid the black background (should answer your question @ nguyแป…n-anh-viแป‡t)
  • Remove unnecessary currentPositionTouched and originalPosition
  • Set the following parameters:
    • minimumVelocityToHide : what speed is enough to hide (default is 1500)
    • minimumScreenRatioToHide : how low is enough to hide (default 0.5)
    • animationDuration : how fast we hide / show (default 0.2 s)



Decision

Swift 3 and 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(_:))) 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; } } 
+9
Nov 16 '17 at 21:45
source share

Swift 4.x, Using Pangesture

Easy way

vertical

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

Helper function

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

Hard way

See This -> https://github.com/satishVekariya/DraggableViewController

+9
Feb 07 '18 at 10:10
source share

What you are describing is an interactive custom transient animation. You set up the animation and the moving transition gesture, i.e. Dismissal (or absence) of the submitted view controller. The easiest way to implement it is to combine the UIPanGestureRecognizer with the UIPercentDrivenInteractiveTransition.

My book explains how to do this, and I published examples (from the book). This specific example is a different situation - the transition goes sideways and not down, and this is for the tab bar controller, and not for the presented controller, but the basic idea is absolutely like this:

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

If you download this project and run it, you will see that what is happening is exactly what you are describing, except that it is lateral: if the drag and drop is more than half, we will go over, but if not, we will cancel and go back in place.

+6
Mar 26 '15 at 23:44
source share

I created an easy to use extension.

Your UIViewController with InteractiveViewController is just inherent, and you did InteractiveViewController

call the showInteractive () method from your controller so that it appears as "Interactive".

enter image description here

+6
Nov 21 '16 at 5:55
source share

I figured out a super easy way to do this. Just paste the following code into your view controller:

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 } } 
+5
May 27 '18 at 16:54
source share

Vertical deviation only

 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! }) } } 
+4
Dec 21 '16 at 10:49
source share

The Goal C: Here is the code

in 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]; } 
+3
Nov 04 '16 at 12:29
source share

Here is the extension I made based on @Wilson answer:

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

USING

Inside your view controller, you want to be swipable:

 override func viewDidLoad() { super.viewDidLoad() self.isSwipable() } 

and that would be unacceptable by scrolling from the far left side of the view controller as a navigation controller.

+2
Nov 08 '17 at 20:25
source share

This is my simple class for the Drag ViewController from the axis . Just inherited your class from DraggableViewController.

 MyCustomClass: DraggableViewController 

Work only for submitted 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
Apr 3 '19 at 17:50
source share

You can use UIPanGestureRecognizer to detect custom drag and drop and modal view with it. If the end position is far enough, the view may be rejected or otherwise transferred to its original position.

Open this answer for more information on how to implement something like this.

0
Mar 26 '15 at 23:02
source share
 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() } ) } } 
0
13 '19 13:11
source share



All Articles