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;
}
}
}
Related
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?
How can I make a UIScrollView start scrolling from top to bottom. When i swipe(down) it will scroll down first not up and disappear. What i have now is, i can scroll up first and then down, while scrolling down it's disappear.
Here is my code -
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//for toogle swipe view
if scrollView.contentOffset.y < 120 {
self.swipeMsgView.isHidden = false
}else {
self.swipeMsgView.isHidden = true
}
//for making bottom inset
if scrollView.contentOffset.y < 190 {
var contentInset:UIEdgeInsets = scrollView.contentInset
contentInset.bottom = scrollView.contentOffset.y-100
scrollView.contentInset = contentInset
}
//when swipe down
if scrollView.contentOffset.y == 0 {
if !isScrollDownFirstTime{
UIView.animate(withDuration: 0.5, animations: {
self.dismiss(animated: true, completion: nil)
})
}
}
//for tracking first time scrolling
if scrollView.contentOffset.y > 150 {
isScrollDownFirstTime = false
}
}
It works fine. But i want to disappear the view when a user swipes or scroll down first (swipe gesture is not working). Is there any elegant way to do this with this existing feature?Thank you.
#Serg Smyk, Here how I fixed the problem.
fileprivate var isScrollDownFirstTime = true
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("Content y offet : \(scrollView.contentOffset.y)")
//for toogle swipe view
if scrollView.contentOffset.y < 120 {
self.swipeMsgView.isHidden = false
} else {
self.swipeMsgView.isHidden = true
}
//for making bottom inset
if scrollView.contentOffset.y < 190 {
var contentInset:UIEdgeInsets = scrollView.contentInset
contentInset.bottom = scrollView.contentOffset.y-100
scrollView.contentInset = contentInset
}
//when swipe down
if scrollView.contentOffset.y == 0 {
print(CLASS_NAME+" -- scrollViewDidScroll() -- contentOffset.y = 0")
if !isScrollDownFirstTime{
UIView.animate(withDuration: 0.5, animations: {
self.dismiss(animated: true, completion: nil)
})
}
}
if scrollView.contentOffset.y < 1 {
print(CLASS_NAME+" -- scrollViewDidScroll() -- contentOffset.y<0")
UIView.animate(withDuration: 0.5, animations: {
self.dismiss(animated: true, completion: nil)
})
}
//for tracking first time scrolling
if scrollView.contentOffset.y > 150 {
isScrollDownFirstTime = false
}
}
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
}
}
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
I'm really stuck here. I would like to shrink the Navigation Bar when I scroll down a UITableView and enlarge it again when scrolling up. I managed to alter the size of the Navigation Bar, but the title image is not shrinking with the Navigation Bar.
I want to do it exactly like Safari, and the problem is that the height of my TitleView is shrinking but the width never changes.
Here's the code I've used to get the height of the scrollbar to change.
func scrollViewDidScroll(scrollView: UIScrollView) {
var navbar = navigationController?.navigationBar
var dir:CGPoint = tableview.panGestureRecognizer.translationInView(self.tableview)
var scrollViewHeight = tableview.frame.size.height
var scrollContentSizeHeight = tableview.contentSize.height
var scrollOffset = tableview.contentOffset.y
if (dir.y > 0 && self.formernavstate == "small") {
self.formernavstate = "big"
UIView.animateWithDuration(0.5, delay:0.0, options: UIViewAnimationOptions.AllowAnimatedContent, animations: { () -> Void in
println("")
navbar?.frame.origin.y = 20
self.navigationItem.titleView?.transform = CGAffineTransformMakeScale(0.52, 0.6)
}, completion: nil)
}
if (dir.y < 0 && self.formernavstate == "big") {
self.formernavstate = "small"
navbar?.frame.origin.y = 0
navigationItem.titleView?.transform = CGAffineTransformMakeScale(0.0001, 0.2)
}
}
I implemented a Swift 3 version with a custom "header" and resizing it's height constraint based on #chauhan's answer:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollViewOffset < scrollView.contentOffset.y {
shrinkHeader(shrink: true)
} else if scrollViewOffset > scrollView.contentOffset.y {
shrinkHeader(shrink: false)
}
}
func shrinkHeader(shrink: Bool) {
if shrink {
if self.headerContainerHeightConstraint.constant > CGFloat(minHeaderHeight) {
UIView.animate(withDuration: 0.1, animations: {
self.headerContainerHeightConstraint.constant = self.headerContainerHeightConstraint.constant - 2
self.view.layoutIfNeeded()
})
}
} else {
if self.headerContainerHeightConstraint.constant < CGFloat(maxHeaderHeight) {
UIView.animate(withDuration: 0.1, animations: {
self.headerContainerHeightConstraint.constant = self.headerContainerHeightConstraint.constant + 6
self.view.layoutIfNeeded()
})
}
}
}
}