How to implement the Control center interactive dismiss animation - ios

I'm currently trying to replicate the dismiss interaction of the iOS Control Panel. So far I've been only able to dismiss the view by swiping down. I'm not able to replicate that same elasticity when swiping upwards and the smooth transition when swiping down after swiping up or generally just playing with the view.
Here is my handlePan handler inside the view I'm trying to dismiss:
func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self)
switch recognizer.state {
case .began:
self._displayPosition = self.frame
self._dismissAnimator = UIViewPropertyAnimator(duration: 0.75, curve: .easeOut)
self._dismissAnimator.pauseAnimation()
case .changed:
if translation.y < 0 {
self._dismissAnimator.addAnimations {
self.frame = self._displayPosition.offsetBy(dx: 0, dy: -20)
}
self._dismissAnimator.fractionComplete = -translation.y*0.10/20
} else {
self._dismissAnimator.addAnimations {
self.frame = self._displayPosition.offsetBy(dx: 0, dy: self._offsetFromScreenBottom)
}
self._dismissAnimator.fractionComplete = translation.y / self._offsetFromScreenBottom
}
case .ended:
if self.frame.origin.y < self._displayPosition.origin.y {
self._dismissAnimator.stopAnimation(true)
self._dismissAnimator.addAnimations {
self.frame = self._displayPosition
}
self._dismissAnimator.startAnimation()
} else {
self._dismissAnimator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}
default:
self._dismissAnimator.finishAnimation(at: .start)
}
}
Thanks in advance!

Related

Using UIPanGestureRecognizer to swipe down UIView

I have an IBAction for a UIPanGestureRecognizer in my UIView, I am able to handle the gesture and recognise the state changes from began, cancelled and ended, as well as respond to those changes.
However when using the sender.location to handle the swipe down, the UIView actually moves up once the gesture has began, and then continues to move down. The experience is jarring and I am not sure what I am doing wrong. Does anybody have any ideas ?
func update(_ translation: CGPoint, origin: CGPoint) {
let offSetY = translation.y
cardView.frame.origin.y = offSetY
let multiplier = 1 - (translation.y / 2000)
self.view.alpha = multiplier
}
func cancel(_ origin: CGPoint) {
let animator = UIViewPropertyAnimator(duration: 0.6, dampingRatio: 0.6) {
self.visualEffectView.alpha = 1
self.cardView.alpha = 1.0
self.view.alpha = 1.0
self.cardView.center = origin
}
animator.startAnimation()
}
func finish() {
let animator = UIViewPropertyAnimator(duration: 0.9, dampingRatio: 0.9) {
self.visualEffectView.effect = nil
self.dismiss(animated: true, completion: nil)
}
animator.startAnimation()
}
#IBAction func panGestureAction(_ sender: UIPanGestureRecognizer) {
self.view.backgroundColor = .white
let originalCardPosition = cardView.center
//let cardOriginY = cardView.frame.origin.y
let translation = sender.translation(in: self.cardView)
let origin = sender.location(in: self.cardView)
switch sender.state {
case .changed:
if translation.y > 0 { update(translation, origin: origin) }
case .ended:
let translation = sender.translation(in: self.cardView)
if translation.y > 100 {
finish()
} else {
cardView.alpha = 1.0
cancel(originalCardPosition)
}
case .cancelled:
cancel(originalCardPosition)
default: break
}
}
The problem is that you set the origin.y of the cardView directly to the value of translation.y. You need to add translation.y to the view's original y value determined when the gesture begins.
Add a property to the class:
var originalY: CGFloat = 0
Then in the .began state of the gesture, set it:
originalY = cardView.frame.origin.y
Then in your update method you set the origin:
cardView.frame.origin.y = originalY + offSetY

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

require(toFail) not working for PanGestureRecognizer and PageViewController

Here's the problem I'm having:
I have a PageViewController that navigates between 4 views horizontally and a PanGestureRecognizer on a UIView that brings it up and down vertically. The issue is that the UIView can be panned up, but then it is stuck there. The user can only swipe horizontally with the PageViewController.
The goal:
Be able to pan the UIView up and down AND swipe left and right on the PageViewController. Basically, make the UIView not get stuck on screen.
This is what I have now:
I have a PanGestureRecognizer function in the UIView file that looks
like this:
func handlePan(recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self)
print("HANDLING PAN")
switch recognizer.state {
case .began:
if vel.y < 0 && !scanIsUp {
animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: {
self.frame = self.frame.offsetBy(dx: 0, dy: -603)
})
scanIsUp = true
isScanVisible = true
} else if vel.y > 0 && scanIsUp {
print("vel.y hehehe")
animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: {
self.frame = self.frame.offsetBy(dx: 0, dy: 603)
})
scanIsUp = false
isScanVisible = false
}
animator?.pauseAnimation()
print("paused?")
case .changed:
let translation = recognizer.translation(in: backgroundImage)
if vel.y < 0 && scanIsUp {
animator?.fractionComplete = translation.y / -603
} else if vel.y > 0 && !scanIsUp {
print("vel.y hehehe pt 2")
animator?.fractionComplete = translation.y / 603
}
case .ended:
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
case .possible: break
default: break
}
}
I also have an array of gestureRecognizers that control the PageViewController. I cycle through them and require them to fail so that the user can pan the UIView up and down like this:
if #available(iOS 10.0, *) {
self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(PFScan.handlePan))
self.panGestureRecognizer!.maximumNumberOfTouches = 1
self.panGestureRecognizer!.cancelsTouchesInView = false
self.addGestureRecognizer(self.panGestureRecognizer!)
for recognizer in self.scrollViewGestureRecognizers {
recognizer.require(toFail: panGestureRecognizer!)
}
}
Finally, I initialize the UIView on the main view controller part of the PageViewController like this:
func setUpScanPage(scrollViewGestureRecognizers: [UIGestureRecognizer]) {
scanPage = PFScan(scrollViewGestureRecognizers: scrollViewGestureRecognizers)
self.view.addSubview(scanPage)
}
Does anyone know why the UIView gets stuck?? I have tried everything I know of. Any help would be immensely appreciated.
Thanks a ton in advanced. Cheers,
Theo

Make a pan gesture respond on vertical swipe downs

I implemented a pan gesture in my application to dismiss a view. Basically when a user swipes down, the view dismisses.
However, I want it to work so that it doesn't dismiss if a user swipes diagonally down, or semi-down. I only want it to be responsive on a strict vertical swipe down. Here is my code so far.
var originalPosition: CGPoint?
var currentPositionTouched: CGPoint?
func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
if currentScreen == 0 {
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!
})
}
}
}
}
You are on the right track with currentPositionTouched. In your .began block, set currentPositionTouched to the panGesture's location
currentPositionTouched = panGesture.location(in: view)
Then, in your .changed and .ended block, use a check to determine if the x value is the same and develop your logic accordingly.
if currentPositionTouched.x == panGesture.location(in: view).x
You can use gestureRecognizerShouldBegin. You can set it up to only recognize vertical gestures (i.e. those for which the angle is less than a certain size). For example, in Swift, it is:
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let gesture = gestureRecognizer as? UIPanGestureRecognizer else { return false }
let translation = gesture.translation(in: gesture.view!)
if translation.x != 0 || translation.y != 0 {
let angle = atan2(abs(translation.x), translation.y)
return angle < .pi / 8
}
return false
}
}
Just make sure to set the delegate of the pan gesture recognizer.
Note, I'm not checking to see if it's absolutely vertical (because of variations in the gesture path, it will likely rarely be perfectly vertical), but within some reasonable angle from that.
FYI, this is based upon this Objective-C rendition.

Returning view to original position after pan gesture, swift

I have a UIView (the red one in the image) I can pan in the x-axis. I can return the view back to the original position but I want to return to that position as though it's being pulled by a light spring, or sort of easing back to the original position with some sort of deceleration.
At the moment it weirdly jumps back to that position.
This is my relevant code and an image is attached to show the view in it's maximum position.
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.Ended {
print(backgroundView.center.x) //120.0
print(backgroundView.center.y) //64.0
let xDivision = Double(backgroundView.center.x/120.0)
print(xDivision)
recognizer.view?.center = CGPointMake(120.0, 64.0)
self.view.frame = CGRectMake(backgroundView.center.x, 0, self.view.frame.width , self.view.frame.height)
UIView.animateWithDuration(xDivision, delay: 0.0, usingSpringWithDamping: 2.0, initialSpringVelocity: 2.0, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.view.center.x = 120.0
}, completion: { (true) in
print("Animation Complete")
print(self.backgroundView.center.x)
})
}
let translation: CGPoint = recognizer.translationInView(self.view)
//recognizer.view?.center = CGPointMake((recognizer.view?.center.x)!, (recognizer.view?.center.y)! + translation.y)
recognizer.setTranslation(CGPointMake(0, 0), inView: self.view)
var center: CGPoint = (recognizer.view?.center)!
print(center.x)
center.x += translation.x
if center.x < 50.0 || center.x > 320.0 {
return
}
recognizer.view?.center = center
}
Any ideas on how I can achieve that. A good example of this animation is this app - http://rise.simplebots.co
Thanks.
If you are using Auto Layout, changing the frame or center properties can cause problems. Have you thought about just changing the constraint's constant? Like:
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
switch recognizer.state {
case .Began:
//do something
case .Changed:
let translation = recognizer.translationInView(self.view)
self.leftContraint.constant = self.leftContraint.constant + translation.x
recognizer.setTranslation(.zero, inView: self.view)
case .Ended:
UIView.animateWithDuration(1,
delay: 0,
options: .CurveEaseOut,
animations: {
self.leftContraint.constant = //your value
self.view.layoutIfNeeded()
},
completion: nil)
default: ()
}
}
That helped me with a similar problem.

Resources