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.
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.
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
I'm using a rotation gesture recognizer to rotate a UIView. After I've rotated the view the x and y coordinates of the pan gesture are off their axis and I can't see why.
Here is my current gesture setup:
open func handleRotate(sender: UIRotationGestureRecognizer) {
if sender.state == .began || sender.state == .changed {
sender.view?.transform = (sender.view?.transform)!.rotated(by: sender.rotation)
sender.rotation = 0
}
}
#objc fileprivate func handleDrag(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: self)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y + translation.y)
sender.setTranslation(.zero, in: self)
}
So when I rotate the UIView the x and y panning are off (Moving up moves the uiview to the left or right depending upon the UIview's rotated state).
Thanks
There's quite a lot to unpack with the code example provided, but for starters the drag handler is setting the center point of the view, which might be a hint as to why the rotation gesture is askew.
Rather than setting the transform property of the view independently in each handler, you might get the result you're looking for if you modify the existing transforms.
The SO answer here goes into a bit more depth, and might help:
Using multiple UIGestureRecognizers simultaneously like UIRotationGestureRecognizer & UIPanGestureRecognizer in Swift 3
I have the following code that drags a UIView. All works fine visually.
func moveView(sender: UIPanGestureRecognizer) {
let translate = sender.translationInView(self.view)
if sender.state == UIGestureRecognizerState.Changed {
sender.view!.center = CGPoint(x:sender.view!.center.x + translate.x, y:sender.view!.center.y + translate.y)
sender.setTranslation(CGPointZero, inView: self.view)
}
if sender.state == UIGestureRecognizerState.Ended {
let newX: CGFloat = sender.view!.center.x + translate.x
let newY: CGFloat = sender.view!.center.y + translate.y
sender.view!.center = CGPoint(x:newX, y:newY)
}
}
However after completing this drag, the view seems to lose the gesture connection such that I can't drag it again or trigger any tap gesture associated with it etc.
If I add an NSLog I can see that tapping where the view used to be triggers the log but not if I tap on the actual current view location.
I establish the gesture to view thisView with the following within viewDidLoad
let moveGesture = UIPanGestureRecognizer(target: self, action: Selector("moveView:"))
thisView.addGestureRecognizer(moveGesture)
What am I missing that keeps the gestures connected to the new view location?
Thanks.
I think that when you call this sender.setTranslation(CGPointZero, inView: self.view) maybe you should set the translation relative to superview and not to the view itself. Setting the translation relative to itself change the position of the view contents but not the view area, which means that if you set the view layer to mask to its bounds you shouldn't see anything that it's outside of the view initial area.
So you should do sender.setTranslation(CGPointZero, inView: self.view.superview).
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")
}
}