UIPangestureRecognizer - One Image Interects with Another - ios

I am stumped on how to get a certain behavior out of my experiment with UIPanGestureRecognizer.
When a user taps and holds an image, and it then intersects with another image, a behavior is triggered. In this case, the behavior is that the opacity of the green or red image goes to 100%.
Once a user continues dragging the image around the screen, and breaks the intersect with either the green or red image, I'd like its opacity to return to 50%. My control flow is getting ugly. I keep tweaking it, but am stumped to find the logic that gets me what I want.
visual
#objc func handlePan(sender: UIPanGestureRecognizer) {
let imageOneView = sender.view!
let translation = sender.translation(in: view)
switch sender.state {
case .began, .changed:
imageOneView.center = CGPoint(x: imageOneView.center.x + translation.x, y: imageOne.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: view)
view.bringSubviewToFront(imageOne)
if imageOne.frame.intersects(winImage.frame) {
winImage.alpha = 1
} else {
if imageOne.frame.intersects(loseImage.frame) {
loseImage.alpha = 1
winImage.alpha = 0.5
} else {
return
}
}
case .ended:
if imageOne.frame.intersects(winImage.frame) {
UIView.animate(withDuration: 0.3, animations: {
self.imageOne.alpha = 0.0
})
performSegue(withIdentifier: "winnerDetailView", sender: self)
} else {
if imageOne.frame.intersects(loseImage.frame) {
UIView.animate(withDuration: 0.3, animations: {
self.imageOne.alpha = 0.0
})
navigationItem.rightBarButtonItem?.isEnabled = true
loseImage.alpha = 0.5
} else {
UIView.animate(withDuration: 0.3) {
self.imageOne.frame.origin = self.imageOneOrgin
}
}
}
default:
break
}
}
And additional code: https://github.com/ericmseitz/imageWar
Thanks.

Couple things...
Control flow looks good.
To get the "win" and "lost" alpha reset, set them both to 0.5 on entry to handlePan(). If intersection is detected, set the appropriate one to 1.0. It won't update until the UI updates, which won't happen inside this function.
If you have your draggable views embedded in stack views (as you have in your GitHub repo), you won't be able to bring them in front of each other.
Also, if you have your draggable views embedded in stack views (or other views) make sure to convert the frame to the main view's coordinate space.
Here is modified code, based on what you posted in your question and the layout (stack view embedded) in your repo:
class ViewController: UIViewController {
// IBOutlets
#IBOutlet weak var winImage: UIImageView!
#IBOutlet weak var loseImage: UIImageView!
#IBOutlet weak var imageOne: UIImageView!
#IBOutlet weak var imageTwo: UIImageView!
#IBOutlet weak var imageThree: UIImageView!
#IBOutlet weak var imageFour: UIImageView!
// Constants and Variables
var currentImageCenter = CGPoint.zero
override func viewDidLoad() {
super.viewDidLoad()
[imageOne, imageTwo, imageThree, imageFour].forEach {
let p = UIPanGestureRecognizer(target: self, action: #selector(handlePan(sender:)))
$0?.addGestureRecognizer(p)
}
winImage.alpha = 0.5
loseImage.alpha = 0.5
}
#objc func handlePan(sender: UIPanGestureRecognizer) {
guard let imageOneView = sender.view else { return }
let translation = sender.translation(in: view)
// set alpha for both to 0.5
// if either is intersected, it will be set to 1.0
winImage.alpha = 0.5
loseImage.alpha = 0.5
switch sender.state {
case .began:
// track center of drag-view so we can return if needed
currentImageCenter = imageOneView.center
case .changed:
imageOneView.center = CGPoint(x: imageOneView.center.x + translation.x, y: imageOneView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: view)
// convert drag-view frame to view coordinate space
guard let r = view.getConvertedFrame(fromSubview: imageOneView) else { return }
view.bringSubviewToFront(imageOneView)
if r.intersects(winImage.frame) {
winImage.alpha = 1
} else {
if r.intersects(loseImage.frame) {
loseImage.alpha = 1
} else {
return
}
}
case .ended:
// convert drag-view frame to view coordinate space
guard let r = view.getConvertedFrame(fromSubview: imageOneView) else { return }
if r.intersects(winImage.frame) {
UIView.animate(withDuration: 0.3, animations: {
imageOneView.alpha = 0.0
})
//performSegue(withIdentifier: "winnerDetailView", sender: self)
} else {
if r.intersects(loseImage.frame) {
UIView.animate(withDuration: 0.3, animations: {
imageOneView.alpha = 0.0
})
navigationItem.rightBarButtonItem?.isEnabled = true
} else {
UIView.animate(withDuration: 0.3) {
imageOneView.center = self.currentImageCenter
}
}
}
default:
break
}
}
}
Note: the above needs this UIView extension to convert the frame coordinates:
// from Answer on StackOverflow: https://stackoverflow.com/a/57261226/6257435
extension UIView {
// there can be other views between `subview` and `self`
func getConvertedFrame(fromSubview subview: UIView) -> CGRect? {
// check if `subview` is a subview of self
guard subview.isDescendant(of: self) else {
return nil
}
var frame = subview.frame
if subview.superview == nil {
return frame
}
var superview = subview.superview
while superview != self {
frame = superview!.convert(frame, to: superview!.superview)
if superview!.superview == nil {
break
} else {
superview = superview!.superview
}
}
return superview!.convert(frame, to: self)
}
}

Related

A way to add Instagram like layout guide with UIPinchGestureRecognizer UIRotationGestureRecognizer & UIPanGestureRecognizer?

I have used UIPinchGestureRecognizer UIPanGestureRecognizer & UIRotationGestureRecognizer with UILabel to achieve Instagram like zoom and drag functionality. Now I would like to show layout guide like when UILabel is dragged in center it should show layout guide like below example. It should also display layout guide when you rotate UILabel.
What is the best and accurate possible way to achieve this functionality?
This is what I already have
(Image taken from this question by #Skiddswarmik)
Here is code I have for simple drag and zoom functionality (taken from this answer by #lbsweek)
SnapGesture Class
import UIKit
/*
usage:
add gesture:
yourObjToStoreMe.snapGesture = SnapGesture(view: your_view)
remove gesture:
yourObjToStoreMe.snapGesture = nil
disable gesture:
yourObjToStoreMe.snapGesture.isGestureEnabled = false
advanced usage:
view to receive gesture(usually superview) is different from view to be transformed,
thus you can zoom the view even if it is too small to be touched.
yourObjToStoreMe.snapGesture = SnapGesture(transformView: your_view_to_transform, gestureView: your_view_to_recieve_gesture)
*/
class SnapGesture: NSObject, UIGestureRecognizerDelegate {
// MARK: - init and deinit
convenience init(view: UIView) {
self.init(transformView: view, gestureView: view)
}
init(transformView: UIView, gestureView: UIView) {
super.init()
self.addGestures(v: gestureView)
self.weakTransformView = transformView
}
deinit {
self.cleanGesture()
}
// MARK: - private method
private weak var weakGestureView: UIView?
private weak var weakTransformView: UIView?
private var panGesture: UIPanGestureRecognizer?
private var pinchGesture: UIPinchGestureRecognizer?
private var rotationGesture: UIRotationGestureRecognizer?
private func addGestures(v: UIView) {
panGesture = UIPanGestureRecognizer(target: self, action: #selector(panProcess(_:)))
v.isUserInteractionEnabled = true
panGesture?.delegate = self // for simultaneous recog
v.addGestureRecognizer(panGesture!)
pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchProcess(_:)))
//view.isUserInteractionEnabled = true
pinchGesture?.delegate = self // for simultaneous recog
v.addGestureRecognizer(pinchGesture!)
rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotationProcess(_:)))
rotationGesture?.delegate = self
v.addGestureRecognizer(rotationGesture!)
self.weakGestureView = v
}
private func cleanGesture() {
if let view = self.weakGestureView {
//for recognizer in view.gestureRecognizers ?? [] {
// view.removeGestureRecognizer(recognizer)
//}
if panGesture != nil {
view.removeGestureRecognizer(panGesture!)
panGesture = nil
}
if pinchGesture != nil {
view.removeGestureRecognizer(pinchGesture!)
pinchGesture = nil
}
if rotationGesture != nil {
view.removeGestureRecognizer(rotationGesture!)
rotationGesture = nil
}
}
self.weakGestureView = nil
self.weakTransformView = nil
}
// MARK: - API
private func setView(view:UIView?) {
self.setTransformView(view, gestgureView: view)
}
private func setTransformView(_ transformView: UIView?, gestgureView:UIView?) {
self.cleanGesture()
if let v = gestgureView {
self.addGestures(v: v)
}
self.weakTransformView = transformView
}
open func resetViewPosition() {
UIView.animate(withDuration: 0.4) {
self.weakTransformView?.transform = CGAffineTransform.identity
}
}
open var isGestureEnabled = true
// MARK: - gesture handle
// location will jump when finger number change
private var initPanFingerNumber:Int = 1
private var isPanFingerNumberChangedInThisSession = false
private var lastPanPoint:CGPoint = CGPoint(x: 0, y: 0)
#objc func panProcess(_ recognizer:UIPanGestureRecognizer) {
if isGestureEnabled {
//guard let view = recognizer.view else { return }
guard let view = self.weakTransformView else { return }
// init
if recognizer.state == .began {
lastPanPoint = recognizer.location(in: view)
initPanFingerNumber = recognizer.numberOfTouches
isPanFingerNumberChangedInThisSession = false
}
// judge valid
if recognizer.numberOfTouches != initPanFingerNumber {
isPanFingerNumberChangedInThisSession = true
}
if isPanFingerNumberChangedInThisSession {
return
}
// perform change
let point = recognizer.location(in: view)
view.transform = view.transform.translatedBy(x: point.x - lastPanPoint.x, y: point.y - lastPanPoint.y)
lastPanPoint = recognizer.location(in: view)
}
}
private var lastScale:CGFloat = 1.0
private var lastPinchPoint:CGPoint = CGPoint(x: 0, y: 0)
#objc func pinchProcess(_ recognizer:UIPinchGestureRecognizer) {
if isGestureEnabled {
guard let view = self.weakTransformView else { return }
// init
if recognizer.state == .began {
lastScale = 1.0;
lastPinchPoint = recognizer.location(in: view)
}
// judge valid
if recognizer.numberOfTouches < 2 {
lastPinchPoint = recognizer.location(in: view)
return
}
// Scale
let scale = 1.0 - (lastScale - recognizer.scale);
view.transform = view.transform.scaledBy(x: scale, y: scale)
lastScale = recognizer.scale;
// Translate
let point = recognizer.location(in: view)
view.transform = view.transform.translatedBy(x: point.x - lastPinchPoint.x, y: point.y - lastPinchPoint.y)
lastPinchPoint = recognizer.location(in: view)
}
}
#objc func rotationProcess(_ recognizer: UIRotationGestureRecognizer) {
if isGestureEnabled {
guard let view = self.weakTransformView else { return }
view.transform = view.transform.rotated(by: recognizer.rotation)
recognizer.rotation = 0
}
}
//MARK:- UIGestureRecognizerDelegate Methods
func gestureRecognizer(_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
return true
}
}
Add Gesture in UILabel
// define
var snapGesture: SnapGesture?
// add gesture
self.snapGesture = SnapGesture(view: self.myLabel!)
Below you will find an updated version of your class that should do what you describe.
Most of the updated code is located at the last section (Guides) near the end, but I have updated your UIGestureRecognizer actions a bit as well as your main init method.
Features:
- A vertical guide for centering a view's position horizontally.
- A horizontal guide for centering a view's rotation at 0 degrees.
- Position and rotation snapping to guides with tolerance values (snapToleranceDistance and snapToleranceAngle properties).
- Animated appearance / disappearance of guides (animateGuides and guideAnimationDuration properties).
- Guide views that can be changed per use case (movementGuideView and rotationGuideView properties)
class SnapGesture: NSObject, UIGestureRecognizerDelegate {
// MARK: - init and deinit
convenience init(view: UIView) {
self.init(transformView: view, gestureView: view)
}
init(transformView: UIView, gestureView: UIView) {
super.init()
self.addGestures(v: gestureView)
self.weakTransformView = transformView
guard let transformView = self.weakTransformView, let superview = transformView.superview else {
return
}
// This is required in order to be able to snap the view to center later on,
// using the `tx` property of its transform.
transformView.center = superview.center
}
deinit {
self.cleanGesture()
}
// MARK: - private method
private weak var weakGestureView: UIView?
private weak var weakTransformView: UIView?
private var panGesture: UIPanGestureRecognizer?
private var pinchGesture: UIPinchGestureRecognizer?
private var rotationGesture: UIRotationGestureRecognizer?
private func addGestures(v: UIView) {
panGesture = UIPanGestureRecognizer(target: self, action: #selector(panProcess(_:)))
v.isUserInteractionEnabled = true
panGesture?.delegate = self // for simultaneous recog
v.addGestureRecognizer(panGesture!)
pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchProcess(_:)))
//view.isUserInteractionEnabled = true
pinchGesture?.delegate = self // for simultaneous recog
v.addGestureRecognizer(pinchGesture!)
rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotationProcess(_:)))
rotationGesture?.delegate = self
v.addGestureRecognizer(rotationGesture!)
self.weakGestureView = v
}
private func cleanGesture() {
if let view = self.weakGestureView {
//for recognizer in view.gestureRecognizers ?? [] {
// view.removeGestureRecognizer(recognizer)
//}
if panGesture != nil {
view.removeGestureRecognizer(panGesture!)
panGesture = nil
}
if pinchGesture != nil {
view.removeGestureRecognizer(pinchGesture!)
pinchGesture = nil
}
if rotationGesture != nil {
view.removeGestureRecognizer(rotationGesture!)
rotationGesture = nil
}
}
self.weakGestureView = nil
self.weakTransformView = nil
}
// MARK: - API
private func setView(view:UIView?) {
self.setTransformView(view, gestgureView: view)
}
private func setTransformView(_ transformView: UIView?, gestgureView:UIView?) {
self.cleanGesture()
if let v = gestgureView {
self.addGestures(v: v)
}
self.weakTransformView = transformView
}
open func resetViewPosition() {
UIView.animate(withDuration: 0.4) {
self.weakTransformView?.transform = CGAffineTransform.identity
}
}
open var isGestureEnabled = true
// MARK: - gesture handle
// location will jump when finger number change
private var initPanFingerNumber:Int = 1
private var isPanFingerNumberChangedInThisSession = false
private var lastPanPoint:CGPoint = CGPoint(x: 0, y: 0)
#objc func panProcess(_ recognizer:UIPanGestureRecognizer) {
guard isGestureEnabled, let view = self.weakTransformView else { return }
// init
if recognizer.state == .began {
lastPanPoint = recognizer.location(in: view)
initPanFingerNumber = recognizer.numberOfTouches
isPanFingerNumberChangedInThisSession = false
}
// judge valid
if recognizer.numberOfTouches != initPanFingerNumber {
isPanFingerNumberChangedInThisSession = true
}
if isPanFingerNumberChangedInThisSession {
hideGuidesOnGestureEnd(recognizer)
return
}
// perform change
let point = recognizer.location(in: view)
view.transform = view.transform.translatedBy(x: point.x - lastPanPoint.x, y: point.y - lastPanPoint.y)
lastPanPoint = recognizer.location(in: view)
updateMovementGuide()
hideGuidesOnGestureEnd(recognizer)
}
private var lastScale:CGFloat = 1.0
private var lastPinchPoint:CGPoint = CGPoint(x: 0, y: 0)
#objc func pinchProcess(_ recognizer:UIPinchGestureRecognizer) {
guard isGestureEnabled, let view = self.weakTransformView else { return }
// init
if recognizer.state == .began {
lastScale = 1.0;
lastPinchPoint = recognizer.location(in: view)
}
// judge valid
if recognizer.numberOfTouches < 2 {
lastPinchPoint = recognizer.location(in: view)
hideGuidesOnGestureEnd(recognizer)
return
}
// Scale
let scale = 1.0 - (lastScale - recognizer.scale);
view.transform = view.transform.scaledBy(x: scale, y: scale)
lastScale = recognizer.scale;
// Translate
let point = recognizer.location(in: view)
view.transform = view.transform.translatedBy(x: point.x - lastPinchPoint.x, y: point.y - lastPinchPoint.y)
lastPinchPoint = recognizer.location(in: view)
updateMovementGuide()
hideGuidesOnGestureEnd(recognizer)
}
#objc func rotationProcess(_ recognizer: UIRotationGestureRecognizer) {
guard isGestureEnabled, let view = self.weakTransformView else { return }
view.transform = view.transform.rotated(by: recognizer.rotation)
recognizer.rotation = 0
updateRotationGuide()
hideGuidesOnGestureEnd(recognizer)
}
func hideGuidesOnGestureEnd(_ recognizer: UIGestureRecognizer) {
if recognizer.state == .ended {
showMovementGuide(false)
showRotationGuide(false)
}
}
// MARK:- UIGestureRecognizerDelegate Methods
func gestureRecognizer(_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
return true
}
// MARK:- Guides
var animateGuides = true
var guideAnimationDuration: TimeInterval = 0.3
var snapToleranceDistance: CGFloat = 5 // pts
var snapToleranceAngle: CGFloat = 1 // degrees
* CGFloat.pi / 180 // (converted to radians)
var movementGuideView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.blue
return view
} ()
var rotationGuideView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.red
return view
} ()
// MARK: Movement guide and snap
func updateMovementGuide() {
guard let transformView = weakTransformView, let superview = transformView.superview else {
return
}
let transformX = transformView.frame.midX
let superX = superview.bounds.midX
if transformX - snapToleranceDistance < superX && transformX + snapToleranceDistance > superX {
transformView.transform.tx = 0
showMovementGuide(true)
} else {
showMovementGuide(false)
}
updateGuideFrames()
}
var isShowingMovementGuide = false
func showMovementGuide(_ shouldShow: Bool) {
guard isShowingMovementGuide != shouldShow,
let transformView = weakTransformView,
let superview = transformView.superview
else { return }
superview.insertSubview(movementGuideView, belowSubview: transformView)
movementGuideView.frame = CGRect(
x: superview.frame.midX,
y: 0,
width: 1,
height: superview.frame.size.height
)
let duration = animateGuides ? guideAnimationDuration : 0
isShowingMovementGuide = shouldShow
UIView.animate(withDuration: duration) { [weak self] in
self?.movementGuideView.alpha = shouldShow ? 1 : 0
}
}
// MARK: Rotation guide and snap
func updateRotationGuide() {
guard let transformView = weakTransformView else {
return
}
let angle = atan2(transformView.transform.b, transformView.transform.a)
if angle > -snapToleranceAngle && angle < snapToleranceAngle {
transformView.transform = transformView.transform.rotated(by: angle * -1)
showRotationGuide(true)
} else {
showRotationGuide(false)
}
}
var isShowingRotationGuide = false
func showRotationGuide(_ shouldShow: Bool) {
guard isShowingRotationGuide != shouldShow,
let transformView = weakTransformView,
let superview = transformView.superview
else { return }
superview.insertSubview(rotationGuideView, belowSubview: transformView)
let duration = animateGuides ? guideAnimationDuration : 0
isShowingRotationGuide = shouldShow
UIView.animate(withDuration: duration) { [weak self] in
self?.rotationGuideView.alpha = shouldShow ? 1 : 0
}
}
func updateGuideFrames() {
guard let transformView = weakTransformView,
let superview = transformView.superview
else { return }
rotationGuideView.frame = CGRect(
x: 0,
y: transformView.frame.midY,
width: superview.frame.size.width,
height: 1
)
}
}
For anyone interested, here's a test project using this class.

How to drag and drop an image on delete icon to delete the image?

Hey guys I have just created a sample image view that can be dragged inside the view.
My Question is how to delete the image when it is dragged and placed in the delete icon?
Can anyone help with swift code
Herewith I have attached my sample program code below:
import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var Lastscale : CGFloat = 1.0
#IBOutlet weak var imgView: UIImageView!
#IBOutlet weak var deleteIcon: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(recognizer:)))
panGestureRecognizer.delegate = self
imgView.addGestureRecognizer(panGestureRecognizer)
imgView.isUserInteractionEnabled = true
}
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let gview = recognizer.view
if recognizer.state == .began || recognizer.state == .changed {
let translation = recognizer.translation(in: gview?.superview)
gview?.center = CGPoint(x: (gview?.center.x)! + translation.x, y: (gview?.center.y)! + translation.y)
recognizer.setTranslation(CGPoint.zero, in: gview?.superview)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Firstly, it's cleaner to use a switch statement to check for recognizer states. Also I think in this case you'd like to set the transform of the layer while the recognizer translation is changing, in this way you retain the initial frame of the view and always can animate back by setting the transform to its identity. Then if the rectangles of the button and image view layer intersect, hide the imageView, else move back. Like this:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
let gview = recognizer.view
let translation = recognizer.translation(in: gview?.superview)
switch recognizer.state {
case .began, .changed:
imgView.layer.transform = CATransform3DMakeTranslation(translation.x, translation.y, 0)
// OR
// imgView.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .ended:
if deleteIcon.frame.intersects(imgView.layer.frame) {
animateDelete()
} else {
moveBack()
}
default:
moveBack()
}
}
func animateDelete() {
UIView.animate(withDuration: 0.3, animations: {
self.imgView.alpha = 0
}) { _ in
self.imgView.isHidden = true
}
}
func moveBack() {
UIView.animate(withDuration: 0.3) {
self.imgView.transform = CGAffineTransform.identity
}
}

Dragging UIView

I need some help with dragging UIView to reveal, menu view underneath main view.
I have two UIViews.
menuView - includes menu buttons and labels
and mainView - located over menuView.
I want to drag the main view from left edge to show menu items and snap the main view to a specific position. I am able to get the dragging to the right working but I cannot set it back to original position when dragging left.
here is my codes I have been playing around with but no success.
I also wanted to make mainView smaller as I drag it to right.
any help will be greatly appreciated.
Note: PanGesture is attached to mainView.
#IBAction func dragMenu(_ sender: UIPanGestureRecognizer) {
let mview = sender.view!
let originalCenter = CGPoint(x: self.mainView.bounds.width/2, y: self.mainView.bounds.height/2)
switch sender.state {
case .changed:
if let mview = sender.view {
mview.center.x = mview.center.x + sender.translation(in: view).x
sender.setTranslation(CGPoint.zero, in: view)
}
case .ended:
let DraggedHalfWayRight = mview.center.x > view.center.x
if DraggedHalfWayRight {
//dragginToRight
showMenu = !showMenu
self.mainViewRight.constant = 200
self.mainTop.constant = 50
self.mainBottom.constant = 50
self.mainLeft.constant = 200
} else //dragging left and set it back to original position.
{
mview.center = originalCenter
showMenu = !showMenu
}
default:
break
}
}
I'd suggest a few things:
When you're done dragging the menu off, make sure to set its alpha to zero (so that, if you were in portrait and go to landscape, you don't suddenly see the menu sitting there).
I personally just adjust the transform of the dragged view and avoid resetting the translation of the gesture back to zero all the time. That's a matter of personal preference.
I'd make the view controller the delegate for the gesture and implement gestureRecognizerShouldBegin (because if the menu is hidden, you don't want to recognize swipes to try to hide it again; and if it's not hidden, you don't want to recognize swipes to show it).
When figuring out whether to complete the gesture or not, I also consider the velocity of the gesture (e.g. so a little flick will dismiss/show the animated view).
Thus:
class ViewController: UIViewController {
#IBOutlet weak var menuView: UIView!
var isMenuVisible = true
#IBAction func dragMenu(_ gesture: UIPanGestureRecognizer) {
let translationX = gesture.translation(in: gesture.view!).x
switch gesture.state {
case .began:
// if the menu is not visible, make sure it's off screen and then make it visible
if !isMenuVisible {
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
menuView.alpha = 1
}
fallthrough
case .changed:
if isMenuVisible {
menuView.transform = CGAffineTransform(translationX: translationX, y: 0)
} else {
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width + translationX, y: 0)
}
case .ended:
let shouldComplete: Bool
if isMenuVisible {
shouldComplete = translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x > 0
} else {
shouldComplete = -translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x < 0
}
UIView.animate(withDuration: 0.25, animations: {
if self.isMenuVisible && shouldComplete || !self.isMenuVisible && !shouldComplete {
self.menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
} else {
self.menuView.transform = .identity
}
if shouldComplete{
self.isMenuVisible = !self.isMenuVisible
}
}, completion: { _ in
self.menuView.alpha = self.isMenuVisible ? 1 : 0
})
default:
break
}
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
guard let gesture = gesture as? UIPanGestureRecognizer else { return true }
let translationX = gesture.translation(in: gesture.view!).x
return isMenuVisible && translationX > 0 || !isMenuVisible && translationX < 0
}
}
Personally, I prefer to use a separate screen edge gesture recognizer to pull them menu back on screen (you don't really want it to recognize gestures anywhere, but just on that right edge). Another virtue of this approach is that by keeping "show" and "hide" in different functions, the code is a lot more readable (IMHO):
class ViewController: UIViewController {
#IBOutlet weak var menuView: UIView!
var isMenuVisible = true
#IBAction func handleScreenEdgeGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
let translationX = gesture.translation(in: gesture.view!).x
switch gesture.state {
case .began:
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
menuView.alpha = 1
fallthrough
case .changed:
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width + translationX, y: 0)
case .ended:
let shouldComplete = -translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x < 0
UIView.animate(withDuration: 0.25, delay:0, options: .curveEaseOut, animations: {
if shouldComplete {
self.menuView.transform = .identity
self.isMenuVisible = !self.isMenuVisible
} else {
self.menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
}
}, completion: { _ in
self.menuView.alpha = self.isMenuVisible ? 1 : 0
})
default:
break
}
}
#IBAction func dragMenu(_ gesture: UIPanGestureRecognizer) {
let translationX = gesture.translation(in: gesture.view!).x
switch gesture.state {
case .began, .changed:
menuView.transform = CGAffineTransform(translationX: translationX, y: 0)
case .ended:
let shouldComplete = translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x > 0
UIView.animate(withDuration: 0.25, delay:0, options: .curveEaseOut, animations: {
if shouldComplete {
self.menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
self.isMenuVisible = !self.isMenuVisible
} else {
self.menuView.transform = .identity
}
}, completion: { _ in
self.menuView.alpha = self.isMenuVisible ? 1 : 0
})
default:
break
}
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIScreenEdgePanGestureRecognizer {
return !isMenuVisible
} else if let gesture = gestureRecognizer as? UIPanGestureRecognizer {
let translationX = gesture.translation(in: gesture.view!).x
return isMenuVisible && translationX > 0
}
return true
}
}

Draggable UIView Swift 3

I want to be able to drag the objects on the screen, but they wont. I tried everything but still cant.
Here are the code.
func panGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
print("Began.")
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.removeItem(forms[i])
}
}
case .changed:
let translation = gesture.translation(in: forms[1])
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: self.view)
print("\(gesture.view!.center.x)=\(gesture.view!.center.y)")
print("t;: \(translation)")
case .ended:
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.addItem(forms[i])
}
}
print("Ended.")
case .cancelled:
print("Cancelled")
default:
print("Default")
}
}
Also they have gravity. The forms are squares and circles.
Explanation:
in .began - i disable the gravity for selected form.
in .changed - i try to change the coordinates.
in .end - i enable again gravity.
ScreenShot.
Step 1 : Take one View which you want to drag in storyBoard.
#IBOutlet weak var viewDrag: UIView!
Step 2 : Add PanGesture.
var panGesture = UIPanGestureRecognizer()
Step 3 : In ViewDidLoad adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
panGesture = UIPanGestureRecognizer(target: self, action: #selector(ViewController.draggedView(_:)))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 4 : Code for draggedView.
func draggedView(_ sender:UIPanGestureRecognizer){
self.view.bringSubview(toFront: viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Step 5 : Output.
It's very easy if you subclass a view:
DraggableView...
class DraggableView: UIIView {
var fromleft: NSLayoutConstraint!
var fromtop: NSLayoutConstraint!
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
fromleft = constraint(id: "fromleft")!
fromtop = constraint(id: "fromtop")!
}
}
override func common() {
super.common()
let p = UIPanGestureRecognizer(
target: self, action: #selector(drag))
addGestureRecognizer(p)
}
#objc func drag(_ s:UIPanGestureRecognizer) {
let t = s.translation(in: self.superview)
fromleft.constant = fromleft.constant + t.x
fromtop.constant = fromtop.constant + t.y
s.setTranslation(CGPoint.zero, in: self.superview)
}
}
Drop a UIView in your scene.
As normal, add a constraint from the left (that's the x position) and add a constraint from the top (that's the y position).
In storyboard simply simply name the constraints "fromleft" and "fromtop"
You're done.
It now works perfectly - that's it.
What is that handy constraint( call ?
Notice the view simply finds its own constraints by name.
In Xcode there is STILL no way to use constraints like IBOutlets. Fortunately it is very easy to find them by "identifier". (Indeed, this is the very purpose of the .identifier feature on constraints.)
extension UIView {
func constraint(id: String) -> NSLayoutConstraint? {
let cc = self.allConstraints()
for c in cc { if c.identifier == id { return c } }
//print("someone forgot to label constraint \(id)") //heh!
return nil
}
func allConstraints() -> [NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { c in
return c.firstItem as? UIView == self ||
c.secondItem as? UIView == self
}
}
Tip...edge versus center!
Don't forget when you make the constraints on a view (as in the image above):
you can set the left one to be either:
to the left edge of the white box, or,
to the center of the white box.
Choose the correct one for your situation. It will make it much easier to do calculations, set sliders, etc.
Footnote - an "initializing" UIView, UI "I" View,
// UI "I" View ... "I" for initializing
// Simply saves you typing inits everywhere
import UIKit
class UIIView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
common()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
common()
}
func common() { }
}
Use below code for Swift 5.0
Step 1 : Take one UIView from Storyboard, drag it into your ViewController file and Create IBOutlet of UIView.
#IBOutlet weak var viewDrag: UIView!
var panGesture = UIPanGestureRecognizer()
Step 2 : In viewDidLoad() adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action:(Selector(("draggedView:"))))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 3 : Create func and add code to move the UIView as like below.
func draggedView(sender:UIPanGestureRecognizer){
self.view.bringSubviewToFront(viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Hope this will help someone.
This UIView extension makes a UIView object draggable and limits the movement to stay within the bounds of the screen.
extension UIView {
func makeDraggable() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.addGestureRecognizer(panGesture)
}
#objc func handlePan(_ gesture: UIPanGestureRecognizer) {
guard gesture.view != nil else { return }
let translation = gesture.translation(in: gesture.view?.superview)
var newX = gesture.view!.center.x + translation.x
var newY = gesture.view!.center.y + translation.y
let halfWidth = gesture.view!.bounds.width / 2.0
let halfHeight = gesture.view!.bounds.height / 2.0
// Limit the movement to stay within the bounds of the screen
newX = max(halfWidth, newX)
newX = min(UIScreen.main.bounds.width - halfWidth, newX)
newY = max(halfHeight, newY)
newY = min(UIScreen.main.bounds.height - halfHeight, newY)
gesture.view?.center = CGPoint(x: newX, y: newY)
gesture.setTranslation(CGPoint.zero, in: gesture.view?.superview)
}
}

In iOS, how to drag down to dismiss a modal?

A common way to dismiss a modal is to swipe down - How do we allows the user to drag the modal down, if it's far enough, the modal's dismissed, otherwise it animates back to the original position?
For example, we can find this used on the Twitter app's photo views, or Snapchat's "discover" mode.
Similar threads point out that we can use a UISwipeGestureRecognizer and [self dismissViewControllerAnimated...] to dismiss a modal VC when a user swipes down. But this only handles a single swipe, not letting the user drag the modal around.
I just created a tutorial for interactively dragging down a modal to dismiss it.
http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/
I found this topic to be confusing at first, so the tutorial builds this out step-by-step.
If you just want to run the code yourself, this is the repo:
https://github.com/ThornTechPublic/InteractiveModal
This is the approach I used:
View Controller
You override the dismiss animation with a custom one. If the user is dragging the modal, the interactor kicks in.
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? {
DismissAnimator()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
interactor.hasStarted ? interactor : .none
}
}
Dismiss Animator
You create a custom animator. This is a custom animation that you package inside a UIViewControllerAnimatedTransitioning protocol.
import UIKit
class DismissAnimator : NSObject {
let transitionDuration = 0.6
}
extension DismissAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
transitionDuration
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView = transitionContext.containerView()
else {
return
}
if transitionContext.transitionWasCancelled {
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
You subclass UIPercentDrivenInteractiveTransition so that it can act as your state machine. Since the interactor object is accessed by both VCs, use it to keep track of the panning progress.
import UIKit
class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}
Modal View Controller
This maps the pan gesture state to interactor method calls. The translationInView() y value determines whether the user crossed a threshold. When the pan gesture is .Ended, the interactor either finishes or cancels.
import UIKit
class ModalViewController: UIViewController {
var interactor:Interactor? = nil
#IBAction func close(sender: UIButton) {
dismiss(animated: true)
}
#IBAction func handleGesture(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
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 interactor = interactor else { return }
switch sender.state {
case .began:
interactor.hasStarted = true
dismiss(animated: true)
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
}
}
}
I'll share 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
}
}
Where the Modals View Controllers inherit from a class that I've built (ViewControllerPannable) to make them draggable and dismissible when reach certain velocity.
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!)
}
#objc 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!
})
}
}
}
}
Here is a one-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 goes down:
Avoid horizontal translation by only updating the y coordinate of view.frame.origin
Avoid panning out of the screen when swiping up with let y = max(0, translation.y)
Also dismiss the view controller based on where the finger is released (defaults to the bottom half of the screen) and not just based on the velocity of the swipe
Show view controller as modal to ensure the previous viewcontroller appears behind and avoid a black background (should answer your question #nguyễn-anh-việt)
Remove unneeded currentPositionTouched and originalPosition
Expose the following parameters:
minimumVelocityToHide: what speed is enough to hide (defaults to 1500)
minimumScreenRatioToHide: how low is enough to hide (defaults to 0.5)
animationDuration : how fast do we hide/show (defaults to 0.2s)
Solution
Swift 3 & Swift 4 :
//
// PannableViewController.swift
//
import UIKit
class PannableViewController: UIViewController {
public var minimumVelocityToHide: CGFloat = 1500
public var minimumScreenRatioToHide: CGFloat = 0.5
public var animationDuration: TimeInterval = 0.2
override func viewDidLoad() {
super.viewDidLoad()
// Listen for pan gesture
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
view.addGestureRecognizer(panGesture)
}
#objc func onPan(_ panGesture: UIPanGestureRecognizer) {
func slideViewVerticallyTo(_ y: CGFloat) {
self.view.frame.origin = CGPoint(x: 0, y: y)
}
switch panGesture.state {
case .began, .changed:
// If pan started or is ongoing then
// slide the view to follow the finger
let translation = panGesture.translation(in: view)
let y = max(0, translation.y)
slideViewVerticallyTo(y)
case .ended:
// If pan ended, decide it we should close or reset the view
// based on the final position and the speed of the gesture
let translation = panGesture.translation(in: view)
let velocity = panGesture.velocity(in: view)
let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
(velocity.y > minimumVelocityToHide)
if closing {
UIView.animate(withDuration: animationDuration, animations: {
// If closing, animate to the bottom of the view
self.slideViewVerticallyTo(self.view.frame.size.height)
}, completion: { (isCompleted) in
if isCompleted {
// Dismiss the view when it dissapeared
dismiss(animated: false, completion: nil)
}
})
} else {
// If not closing, reset the view to the top
UIView.animate(withDuration: animationDuration, animations: {
slideViewVerticallyTo(0)
})
}
default:
// If gesture state is undefined, reset the view to the top
UIView.animate(withDuration: animationDuration, animations: {
slideViewVerticallyTo(0)
})
}
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen;
modalTransitionStyle = .coverVertical;
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .overFullScreen;
modalTransitionStyle = .coverVertical;
}
}
I figured out super simple way to do this. Just put 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
}
}
created a demo for interactively dragging down to dismiss view controller like snapchat's discover mode. Check this github for sample project.
Swift 4.x, Using Pangesture
Simple way
Horizontal
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
Refer this -> https://github.com/satishVekariya/DraggableViewController
Massively updates the repo for Swift 4.
For Swift 3, I have created the following to present a UIViewController from right to left and dismiss it by pan gesture. I have uploaded this as a GitHub repository.
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
}
}
Easy usage:
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!)
}
}
Only vertical dismiss
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!
})
}
}
I've created an easy to use extension.
Just inherent Your UIViewController with InteractiveViewController and you are done
InteractiveViewController
call method showInteractive() from your controller to show as Interactive.
What you're describing is an interactive custom transition animation. You are customizing both the animation and the driving gesture of a transition, i.e. the dismissal (or not) of a presented view controller. The easiest way to implement it is by combining a UIPanGestureRecognizer with a UIPercentDrivenInteractiveTransition.
My book explains how to do this, and I have posted examples (from the book). This particular example is a different situation - the transition is sideways, not down, and it is for a tab bar controller, not a presented controller - but the basic idea is exactly the same:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p300customAnimation3/ch19p620customAnimation1/Animator.swift
If you download that project and run it, you will see that what is happening is exactly what you are describing, except that it is sideways: if the drag is more than half, we transition, but if not, we cancel and snap back into place.
In Objective C :
Here's the code
inviewDidLoad
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];
}
For those who really wanna dive a little deeper into Custom UIViewController Transition, I recommend this great tutorial from raywenderlich.com.
The original final sample project contains bug. So I fixed it and upload it to Github repo. The proj is in Swift 5, so you can easily run and play it.
Here is a preview:
And it's interactive too!
Happy hacking!
This my simple class for Drag ViewController from axis. Just herited your class from DraggableViewController.
MyCustomClass: DraggableViewController
Work only for presented 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)
}
}
Here is an 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
})
}
}
}
}
USAGE
Inside your view controller you want to be swipable :
override func viewDidLoad() {
super.viewDidLoad()
self.isSwipable()
}
and it will be dismissible by swiping from the extreme left side of the view controller, as a navigation controller.
For Swift 4 + Swift 5, using UIPanGestureRecognizer. Based on #SPatel 's answer above.
Add these two helper functions:
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)
}
To dismiss by dragging down:
class SwipeDownViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// dismiss dragging vertically:
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDragY(_:))))
}
#objc func onDragY(_ sender: UIPanGestureRecognizer) {
let percentThreshold: CGFloat = 0.3
let translation = sender.translation(in: view)
let newY = ensureRange(value: view.frame.minY + translation.y, minimum: 0, maximum: view.frame.maxY)
let progress = progressAlongAxis(newY, view.bounds.height)
view.frame.origin.y = newY // Move view to new position
if sender.state == .ended {
let velocity = sender.velocity(in: view)
if velocity.y >= 300 || progress > percentThreshold {
dismiss(animated: true) // Perform dismiss
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.frame.origin.y = 0 // Revert animation
})
}
}
sender.setTranslation(.zero, in: view)
}
}
To dismiss by dragging right:
class SwipeRightViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// dismiss dragging horizontally:
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDragX(_:))))
}
#objc func onDragX(_ 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 {
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)
}
}
You can use a UIPanGestureRecognizer to detect the user's drag and move the modal view with it. If the ending position is far enough down, the view can be dismissed, or otherwise animated back to its original position.
Check out this answer for more information on how to implement something like this.

Resources