I'm interested in how Apple's Maps app is able to transition from the panning gesture on the sheet to scrolling content gesture in a scrollView or tableView. I put this up as a sort of first stab at recreating it. I've been unable to go from panning the entire sheet to scrolling content when the sheet docks without lifting my finger.
There are a few questions like this and a few libraries out there, but I haven't seen the solution to transition part.
#objc func panGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: view)
switch recognizer.state {
// ** Tracking starting offset
case .began: startingOffset = heightConstraint?.constant ?? 0
// ** Toggle panGesture and tableView to properly switch between gestures
case .changed:
let offset = startingOffset - translation.y
var minOffset: CGFloat = 0
mapView.alpha = 1 - (0.5 * (offset / maxOffset))
// This adds elasticity
if offset < 0 {
minOffset = -(0 - offset)/3
}
// ** Track bottom sheet with pan gesture by finding the diff
// be tween translation and starting offset, then constraint
// this value to be between our top margin and min height
let currentOffset = min(maxOffset, max(minOffset, offset))
heightConstraint?.constant = currentOffset
// ** `offset` == 0 means the sheet is minimized
// `offset` == `maxOffset` means the sheet is open
if currentOffset == 0 {
tableView.contentOffset = .zero
tableView.isScrollEnabled = false
} else if currentOffset == maxOffset {
panGesture.isEnabled = false
panGesture.isEnabled = true
tableView.isScrollEnabled = true
}
case .ended, .cancelled:
guard let offset = heightConstraint?.constant else { return }
// ** Handle last position - if nearer to the top finish out the
// animation to the top, and vice versa
var finalOffset: CGFloat = offset > maxOffset/2 ? maxOffset : 0
let velocity = recognizer.velocity(in: view).y
// ** Toggle tableView scrollability
// Handle "flick" action using `velocity`
if velocity < -100 {
finalOffset = maxOffset
tableView.isScrollEnabled = true
} else if offset > maxOffset/2 && velocity > 200 {
finalOffset = 0
tableView.isScrollEnabled = false
} else {
tableView.isScrollEnabled = offset > maxOffset/2
}
// Dismiss keyboard if dismissing sheet
if finalOffset == 0 {
_ = searchField.resignFirstResponder()
}
// ** Animate to top or bottom docking position when gesture ends
// or is cancelled
heightConstraint?.constant = finalOffset
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: [.curveEaseOut, .allowUserInteraction], animations: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.view.layoutIfNeeded()
strongSelf.mapView.alpha = 1 - (0.5 * (finalOffset / strongSelf.maxOffset))
}, completion: nil)
default: ()
}
}
//UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let offset = heightConstraint?.constant else { return }
// ** Disable panning if scrollView isn't at the top
panGesture.isEnabled = tableView.contentOffset.y <= 0 || offset == 0
// ** Don't scroll if bottom sheet is panning down
if scrollView.contentOffset.y < 0 {
scrollView.isScrollEnabled = false
panGesture.isEnabled = true
}
}
Help?
Related
I created a UIView and a UIImageView which is inside the UIView as a subview, then I added a pan gesture to the UIImageView to slide within the UIView, the image slides now but the problem I have now is when the slider gets to the end of the view if movex > xMax, I want to print this just once print("SWIPPERD movex"). The current code I have there continues to print print("SWIPPERD movex") as long as the user does not remove his/her hand from the UIImageView which is used to slide
private func swipeFunc() {
let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(acknowledgeSwiped(sender:)))
sliderImage.addGestureRecognizer(swipeGesture)
swipeGesture.delegate = self as? UIGestureRecognizerDelegate
}
#objc func acknowledgeSwiped(sender: UIPanGestureRecognizer) {
if let sliderView = sender.view {
let translation = sender.translation(in: self.baseView) //self.sliderView
switch sender.state {
case .began:
startingFrame = sliderImage.frame
viewCenter = baseView.center
fallthrough
case .changed:
if let startFrame = startingFrame {
var movex = translation.x
if movex < -startFrame.origin.x {
movex = -startFrame.origin.x
print("SWIPPERD minmax")
}
let xMax = self.baseView.frame.width - startFrame.origin.x - startFrame.width - 15 //self.sliderView
if movex > xMax {
movex = xMax
print("SWIPPERD movex")
}
var movey = translation.y
if movey < -startFrame.origin.y { movey = -startFrame.origin.y }
let yMax = self.baseView.frame.height - startFrame.origin.y - startFrame.height //self.sliderView
if movey > yMax {
movey = yMax
// print("SWIPPERD min")
}
sliderView.transform = CGAffineTransform(translationX: movex, y: movey)
}
default: // .ended and others:
UIView.animate(withDuration: 0.1, animations: {
sliderView.transform = CGAffineTransform.identity
})
}
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return sliderImage.frame.contains(point)
}
You may want to use the .ended state instead of .changed state, based on your requirements. And you've mentioned you want to get the right direction only. You could try below to determine if the swipe came from right to left, or vice-versa, change as you wish:
let velocity = sender.velocity(in: sender.view)
let rightToLeftSwipe = velocity.x < 0
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
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
}
}
i have been trying to implement scrolling behaviour like snapchat tableview where on scrolling down the tableview moves behind top header and the lists scrolls.
I have tried below code :
But it is not as smooth as snapchat, if anyone has any idea please share.
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let CONTAINER_HEIGHT:CGFloat = 44
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.panGestureRecognizer.view!)
if(translation.y <= 0)
{
//UP DIRECTION
let percent = -translation.y / scrollView.panGestureRecognizer.view!.bounds.size.height
print("PERCENT : \(percent)")
switch (scrollView.panGestureRecognizer.state)
{
case .began:
break;
case .changed:
if(percent <= 1.0 && percent >= 0)
{
var panSize = (CONTAINER_HEIGHT)*percent
panSize = CONTAINER_HEIGHT-panSize
print("Pan Value : \(panSize) -- Percent : \(percent)" )
//panSize += 44
self.bottomSlideTopConstraint.constant = panSize
self.view.layoutIfNeeded()
}
break;
// User is currently dragging the scroll view
case .ended,.cancelled:
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.panGestureRecognizer.view)
if (percent > 0.5 && velocity.y == 0) || velocity.y > 0
{
print("ENDED TRUE")
self.bottomSlideTopConstraint.constant = 0
UIView.animate(withDuration: 0.2, animations: {
self.view.layoutIfNeeded()
})
}
else
{
print("CANCELLED")
self.bottomSlideTopConstraint.constant = 44
UIView.animate(withDuration: 0.2, animations: {
self.view.layoutIfNeeded()
})
}
default:
break;
}
}
}
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