Using UIPanGesture on a UITableView (Swift 4.0) to drag the tableview position (i.e. origin). UIPanGestureRecognizer detecting touch events and consuming them. At some point I want to delegate the events to table so it will scroll its items. How can I do so?
What have I tried:
#objc func tableViewDragged(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.began || gestureRecognizer.state == UIGestureRecognizerState.changed {
let translation = gestureRecognizer.translation(in: self.view)
// if origin moved to map origin it should not scroll above but cell should scroll normally. But returning does not help scrolling cells
if self.tableView.frame.origin == self.mapContainer.frame.origin && translation.y < 0 {
return
}
var frame = self.tableView.frame
let nHeight = (frame.origin.y + translation.y)
if (nHeight < self.view.frame.size.height) {
frame.origin.y = nHeight
self.tableView.frame = frame
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
}
Gesture added to tableView
let gesture = UIPanGestureRecognizer(target: self, action: #selector(self.tableViewDragged(gestureRecognizer:)))
self.tableView.addGestureRecognizer(gesture)
self.tableView.isUserInteractionEnabled = true
gesture.delegate = self
Problem Summary:
For the same pan gesture at any point of tableview (i.e. on any cell), I need to scroll items when tableviews origin got fixed in top left corner of the container. Otherwise I have to move the tableview until its got attached to top left point. Moreover If If nothing to scroll down I need to again move the table below for pan down gesture.
*Problem Usecases: *
initial condition - tableview origin is at middle of the screen,
i.e. (0, Height/2)
pan up gesture - table moved up vertically, but no item scrolls, until origin got stuck to (0,0)
pan up gesture - cell items moved up but not table itself
pan up gesture - cell items moved up until end of the cells
pan down gesture - cell items moved down until index 0, lets say came to 0 index
pan down gesture - tableview moved to down vertically until its origin reached to middle of the screen (0, Height/2)
Best way to do this using tableView's own panGestureRecognizer (without adding new gestureRecognizer), like:
tableView.panGestureRecognizer.addTarget(self, action: #selector(self.tableViewDragged(gestureRecognizer:))
Action
#objc func tableViewDragged(gestureRecognizer: UIPanGestureRecognizer) {
guard tableView.contentOffset.y < 0 else { return }
if gestureRecognizer.state == UIGestureRecognizerState.began || gestureRecognizer.state == UIGestureRecognizerState.changed {
let translation = gestureRecognizer.translation(in: self.view)
// if origin moved to map origin it should not scroll above but cell should scroll normally. But returning does not help scrolling cells
if self.tableView.frame.origin == self.mapContainer.frame.origin && translation.y < 0 {
return
}
var frame = self.tableView.frame
let nHeight = (frame.origin.y + translation.y)
if (nHeight < self.view.frame.size.height) {
frame.origin.y = nHeight
self.tableView.frame = frame
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
}
If I am understanding it correctly so you want to cancel PanGesture when you want to Scroll.
Try this :
func isItemAvailabe(gesture: UISwipeGestureRecognizer) -> Bool {
if gesture.direction == .down {
// check if we have some values in down if yes return true else false
} else if gesture.direction == .up {
// check if we have some values in up if yes return true else false
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panGesture && otherGestureRecognizer == self.scrollGesture && isItemAvailabe(gesture: otherGestureRecognizer) {
return true
}
return false
}
Let me know if this solve your problem or I am not getting it correctly.
Also remember to add this too :
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
You can check more here
I don't know a better solution than subclassing your tableView and blocks its contentOffset in the layoutSubviews of it. Something like :
class MyTableView: UITableView {
var blocksOffsetAtZero = false
override func layoutSubviews() {
super.layoutSubviews()
guard blocksOffsetAtZero else { return }
contentOffset = .zero
}
}
I think this is the solution used in the Maps app. When the user drags the tableView on the home screen, it does not scroll but lifts until it reaches the top of the screen and then scrolls again.
To do so, you add a gesture to your tableview which recognizes simultaneously with the recognizer of the tableView and blocks the contentOffset of the tableView each time the recognizer moves the frame of the tableView.
UPDATE
https://github.com/gaetanzanella/mapLike
Did you implement the delegate correctly ?
// MARK: - UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Try something like :
#objc func tableViewDragged(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.began || gestureRecognizer.state == UIGestureRecognizerState.changed {
let translation = gestureRecognizer.translation(in: self.view)
var frame = self.tableView.frame
let nHeight = (frame.origin.y + translation.y)
if (nHeight < self.view.frame.size.height) {
frame.origin.y = nHeight
self.tableView.frame = frame
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
tableView.blocksOffsetAtZero = true
} else {
tableView.blocksOffsetAtZero = false
}
}
}
The main idea : when you are changing the frame, block the scroll.
You could also use the delegate technique presented by kamaldeep singh bhatia but you will not be able to move first tableView then scroll in a single gesture.
See a example here
Related
I'm using this code to make a UIViewController pannable take from this so post.
class ViewControllerPannable: UIViewController {
var panGestureRecognizer: UIPanGestureRecognizer?
var originalPosition: CGPoint?
var currentPositionTouched: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
view.addGestureRecognizer(panGestureRecognizer!)
}
func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
if panGesture.state == .began {
originalPosition = view.center
currentPositionTouched = panGesture.location(in: view)
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
} else if panGesture.state == .ended {
let velocity = panGesture.velocity(in: view)
if velocity.y >= 1500 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.origin.x,
y: self.view.frame.size.height
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center = self.originalPosition!
})
}
}
}
}
This works great on older phones that don't have a notch. But if the phone has a notch, once you start panning, any views pinned to the safe area jump to the superview. I think the issue is in the
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
But I'm not sure how to make anything that was pinned to the safe area stay that way when panning.
You want to avoid layoutSubviews after you drag the view outside the superview's frame. As long as you are not calling setNeedsLayout manually, you can achieve this by ensuring that last frame.size change happens before crossing outside. Just make enough variables to precisely control for this situation.
I encountered this issue with a custom action-sheet-like presentation. When I pulled the view down to dismiss, there was no problem. But when I pulled the view up to stretch and then down, the safeAreaInsets jumped from 0 to non-zero suddenly.
The reason was because my view stretched upwards but not downwards. Pull up was frame.origin.y change and a frame.size.height change. Downwards was only a frame.origin.y change. However, UIPanGestureRecognizer does not have infinite granularity, and so the jump from negative to positive translation caused origin.y to change at the same time as frame.size.height in the downward direction. This triggered layoutSubviews while the view was partially outside the superview, which meant it ran layout code that suddenly acknowledged a safeAreaInsets change.
My solution is essentially:
if translation < 0 && oldTranslation > 0 || translation > 0 && oldTranslation < 0 {
view.frame = originalFrame
} else {
view.frame = calculate(translation)
}
This forces the view to be a static size before it crosses the superview's boundary, ensuring that layoutSubviews is called with the correct safeAreaInsets, but as the position changes, the safeArea does not.
Your situation may be a little different if you have something else triggering layoutSubviews. You might have to hunt that down.
Another thing I notice is that you're moving the UIViewController's view when it might have a parent, which could have other implications or edge cases because safeArea is inherited from the UIViewController hierarchy rather than solely superview.
I have a UICollectionView that scrolls horizontally. Each item can be swiped upward to delete. This is done through a pan gesture that inserted on the UICollectionViewCell. The animation code is ad follows:
let translation = gesture.translation(in: contentView)
if gesture.state == .changed {
tabView.center.y += translation.y
gesture.setTranslation(CGPoint.zero, in: contentView)
} else if gesture.state == .ended {
if tabView.center.y < contentView.center.y {
UIView.animate(withDuration: 0.2, animations: {
self.tabView.frame.origin.y = -self.tabView.frame.size.height
}) { (_) in
self.delegate?.didSwipeCell(at: self.indexPath())
}
}
}
This is the collection view delegate conformance
func didSwipeCell(at indexPath: IndexPath?) {
guard indexPath != nil else {
return
}
self.tabs.remove(at: indexPath!.row)
self.deleteItems(at: [indexPath!])
}
This produces the following effect:
As you can see, I have 6 items in my collection view, however, when swiping away and deleting the first four items for example, the last 2 items disappear. I have not been able to figure out the reason of the disappearance of those items. Any help is appreciated. Thanks.
I solved this issue by overriding prepareForReuse() in the UICollectionViewCell and setting the frame origin back to (0,0)
I have a vertically scrollable UICollectionView with cells that have an added horizontal pan gesture.
Is there an advised way to disable the horizontal pan gesture while scrolling the UICollectionView? I ask because whenever I do scroll vertically, the slightest movements left or right also trigger the pan gesture and move the cell right or left.
I tried enabling/disabling the UICollectionView when the state of the gesture changed but it was difficult to scroll since a pan gesture would be read in almost every touch instance. I also tried doing something I found online in assessing the x/y velocity and determining whether to fulfill the pan gesture. This seemed to prevent any horizontal panning though..
And finally, I need to use pan versus swipe because I need to include conditional statements on a translation threshold when someone pans (i.e. didn't go x distance, return cell back to original position). If there is a way with swipe, I'm all ears!
Relevant code below:
func onPan(_ sender: UIPanGestureRecognizer) {
//Attempt 1
theCollectionView.isScrollEnabled = false
let cellView = sender.view!
let point = sender.translation(in: cellView)
cellView.center = CGPoint(x: parentView.center.x + point.x, y: cellView.center.y)
if sender.state == .ended {
//Attempt 1
homeCollectionView.isScrollEnabled = true
UIView.animate(withDuration: 0.2, animations: {
cellView.center = CGPoint(x: self.parentView.center.x, y: cellView.center.y)
})
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
//Attempt 2
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return abs((pan.velocity(in: pan.view)).x) > abs((pan.velocity(in: pan.view)).y)
}
I have a subview in a viewController to which I added a UIPanGestureRecognizer. I'd like to expand such subview when the user drags it down, so I'm trying to handle the gesture recognizer status like this:
#objc func panning(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: view)
if gestureRecognizer.state == .began {
originalFrame = self.mySubview.frame
} else if gestureRecognizer.state == .changed {
if translation.y > 0 {
var newFrame = originalFrame
newFrame.size.height += translation.y
mySubview.frame = newFrame
}
}
}
when I set a breakpoint at mySubview.frame = newFrame, I see that newFrame is updated with the translation. However, I don't see the frame changes in the screen, the subview looks always like at the beginning.
What could I be missing?
PanGesture is a tricky, but working with it is very interesting...
I am assuming you have added pan gesture correctly to the view. So writing down the handling of the gesture only.
func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
var translatedPoint: CGPoint = gesture.translation(in: self.superview)
let playerCenterX: CGFloat = (gesture.view?.center.x)! + translatedPoint.x
let playerCenterY: CGFloat = (gesture.view?.center.y)! + translatedPoint.y
translatedPoint = CGPoint.init(x: playerCenterX,
y: playerCenterY)
gesture.view?.center = translatedPoint
gesture.setTranslation(CGPoint.zero, in: self.superview)
}
Important thing is that you set its translation back to zero every time at the end of gesture movement.
As per your requirement change this self.superview and use the coordinates to update your view frame...
If your view's frame is set by the autolayout you should update constraints, not the view's frame itself.
I am attempting to use UIPanGestureRecognizer to translate and dismiss a UITableViewController. I want the gesture to trigger translation of the TableView only when it has reached the bottom of its scroll view and dismiss the TableView when it has been translated 1/3 the height of the screen. I've tried to add the GestureRecognizer when the TableView has reached the bottom of its scroll view, but the application ends up adding the gesture recognizer and disabling the freedom to scroll back up in the TableView.
I can supply code upon request, but I should be able to follow general solutions you may have.
Thanks in advance.
Few extra things to note:
I've added the gesture recognizer to the table view
I want to create a similar effect to a particular view in Facebook's iPhone application. In their app, whenever you select a photo from a photo album, it presents a TableView that allows you to translate and dismiss it whenever you reach the any of the edges in its scrollview.
Here's the code I currently have:
override func scrollViewDidScroll(scrollView: UIScrollView) {
// MARK: - Scrollview dismiss from bottom
let scrollViewHeight = scrollView.frame.height
let scrollContentSizeHeight = scrollView.contentSize.height
let scrollOffset = scrollView.contentOffset.y
// Detects if scroll view is at bottom of table view
if scrollOffset + scrollViewHeight >= scrollContentSizeHeight {
println("Reached bottom of table view")
self.panGesture = UIPanGestureRecognizer(target: self, action: "slideViewFromBottom:")
self.imageTableView.addGestureRecognizer(panGesture)
}
}
func slideViewFromBottom(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(imageTableView).y
let velocity = recognizer.velocityInView(self.imageTableView)
var centerOfImageTableView = self.imageTableView.center.y
switch recognizer.state {
case .Began:
// Touches begin
println("Touches began")
case .Changed:
// Limits view translation to only pan up
if velocity.y < 0.0 {
centerOfImageTableView = self.screenbounds.height/2 + translation
}
case .Ended:
// Determines length of translation to be animated
let moveToTop = screenbounds.height + translation
// Animates view depending on view location at end of gesture
if translation <= -UIScreen.mainScreen().bounds.height/2 {
UIView.animateWithDuration(0.2) {
self.imageTableView.transform = CGAffineTransformMakeTranslation(0.0, -moveToTop)
return
}
delay(0.3) {
self.presentingViewController?.dismissViewControllerAnimated(false, completion: nil)
}
}
else {
UIView.animateWithDuration(0.3) {
self.imageTableView.transform = CGAffineTransformMakeTranslation(0.0, -translation)
return
}
recognizer.enabled = false
}
default:
println("Default executed")
}
}