I want to implement a view that stays at the bottom of the screen and can be expanded with a pan gesture just like in the Uber app for iOS?
Uber Home screen The view will be minimizable when dragged downwards
There are few third party libraries which makes your work easy. This is one of it. LNPopUpController.
Or else, if you want to customise the code and write: Take one view controller and add a UIView on top of it. Now add Pan Gesture to view like below.
func handlePan(pan : UIPanGestureRecognizer) {
let velocity = pan.velocityInView(self.superview).y //; print("Velocity : \(velocity)")
var location = pan.locationInView(self.superview!) //; print("Location : \(location)")
var movement = self.frame
movement.origin.x = 0
movement.origin.y = movement.origin.y + (velocity * 0.05) //; print("Frame.y : \(movement.origin.y)")
if pan.state == .Ended{
print("Gesture Ended")
panGestureEnded()
}else if pan.state == .Began { print("Gesture Began")
let center = self.center
offset.y = location.y - center.y //; print("Offset.y : \(offset.y)")
}else{ print("Gesture else")
animator.removeBehavior(snap)
// Apply the initial offset.
location.x -= offset.x
location.y -= offset.y
//print("location.y : \(location.y)")
// Bound the item position inside the reference view.
location.x = self.superview!.frame.width / 2
// Apply the resulting item center.
snap = UISnapBehavior(item: self, snapToPoint: location)
animator.addBehavior(snap)
}
}
Related
I am working on UIPanGestureRecognizer and to me it is working. but I have some problem here as I am new to iOS and just shifted from Android to iOS.
First take a look at what I want to do:
What I want: I have a UITableView and I want to perform swiping on the Cells. I just want to drag them from left to right side and move/Delete that cell. Pretty same like it is done in android.
But I just want to move the item only in one direction. And that is "LEFT TO RIGHT". But not from right to left. Now here take a look at what I have done so far
What I have Done:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .began {
// when the gesture begins, record the current center location
originalCenter = center
print("Center",originalCenter)
}
// 2
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
center = CGPoint(x: originalCenter.x+translation.x, y: originalCenter.y)
// has the user dragged the item far enough to initiate a delete/complete?
deleteOnDragRelease = frame.origin.x < -frame.size.width / 2.0
completeOnDragRelease = frame.origin.x > frame.size.width / 2.0
// print ("FrameX = ",frame.origin.x , " , ","Width = ",frame.size.width / 2.0 , "Total = ",frame.origin.x < -frame.size.width / 2.0 )
//print ("DelOnDrag = ",deleteOnDragRelease , " , ","CompOnDrag = ",completeOnDragRelease)
}
// 3
if recognizer.state == .ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if deleteOnDragRelease {
if delegate != nil && clickedItem != nil {
// notify the delegate that this item should be deleted
delegate!.toDoItemDeleted(clickedItem: clickedItem!)
}
} else if completeOnDragRelease {
UIView.animate(withDuration: 8.2, animations: {self.frame = originalFrame})
} else {
UIView.animate(withDuration: 8.2, animations: {self.frame = originalFrame})
}
}
}
I know I can make a check on ".changed" , and calculate if the X value is going towards 0 or lesser then 0. But point is for some time it will move item from right to left.
Question: Is there any way I can get the x value of point of contact? or just some how I can get user want to swipe right to left and just stop user from doing that?? Please share your knowledge
your same code just one changes in your UIGestureRecognizer method replace with this code and your problem solve. only left to right side swap work on your tableview cell . any query regrading this just drop comment below.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let translation = panGestureRecognizer.translation(in: superview!)
if translation.x >= 0 {
return true
}
return false
}
return false
}
Good Luck.
Keep coding.
You can do this using below extensions
extension UIPanGestureRecognizer {
enum GestureDirection {
case Up
case Down
case Left
case Right
}
func verticalDirection(target: UIView) -> GestureDirection {
return self.velocity(in: target).y > 0 ? .Down : .Up
}
func horizontalDirection(target: UIView) -> GestureDirection {
return self.velocity(in: target).x > 0 ? .Right : .Left
}
}
And you can get direction like below
gestureRecognizer.horizontalDirection(target: self)
ja a gesture that is on the view that vertical black in the photo.
I want to know its location at the level of the screen, because I want to execute a function if for example I slide the view to the right at more than half of the center of the screen
code:
#objc func detectPan(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.startingConstant = self.centerConstraint.constant
case .changed:
let translation = recognizer.translation(in: self.view)
self.centerConstraint.constant = self.startingConstant - translation.x
default:
break
}
}
Fist image
Second image
You can convert coordinates (points and frames) between views as long as they are on the same hierarchy. You have two methods convert(:to:) and convert(:from:).
In your case you seem to want to convert location in your view to screen which is your key window UIApplication.shared.keyWindow.
So in general the point on screen is let pointOnScreen = myView.convert(pointInMyView, to: UIApplication.shared.keyWindow).
So in your case:
let myView = self.view
let pointInMyView = recognizer.locationInView(myView)
let pointOnScreen = myView.convert(pointInMyView, to: UIApplication.shared.keyWindow)
let isViewGestureOnRightSideOfTheScreen = pointOnScreen.x > UIApplication.shared.keyWindow!.frame.midX
you can get point via
func transform(for translation : CGPoint) -> CGAffineTransform {
let moveBy = CGAffineTransform(translationX: translation.x, y: translation.y)
return moveBy
}
#IBAction func setPanGesture(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .changed:
let translation = sender.translation(in: view)
let translationView = transform(for: translation)
print(translationView)
case .ended:
let translation = sender.translation(in: view)
let translationView = transform(for: translation)
print(translationView)
default:
break
}
}
I am trying to set the limit the bounds of my image view, Or something to prevent the image/ view from zooming out or to the left as it looks pretty bad. Here is the problem Looks Good, Only should be able to zoom in... Zoomed out, Looks Bad
#IBAction func scaleView(_ sender: UIPinchGestureRecognizer) {
//print(self.my_new_fullimage)
self.view.transform = self.view.transform.scaledBy(x: sender.scale, y: sender.scale)
sender.scale = 1
}
#IBAction func panView(_ gestureRecognizer: UIPanGestureRecognizer) {
// Move the anchor point of the view's layer to the touch point
// so that moving the view becomes simpler.
let piece = gestureRecognizer.view
//self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
// Get the distance moved since the last call to this method.
let translation = gestureRecognizer.translation(in: piece?.superview)
// Set the translation point to zero so that the translation distance
// is only the change since the last call to this method.
piece?.center = CGPoint(x: ((piece?.center.x)! + translation.x),
y: ((piece?.center.y)! + translation.y))
gestureRecognizer.setTranslation(CGPoint.zero, in: piece?.superview)
}
}
Anything can help.
-Thanks!
I'm not sure whether this has been asked or not, but I failed to find a solution. I'm implementing panning gesture on a button, but the idea is: the button is fixed to a position, and when the user drags it, a copy of the button is created and moving with the gesture; the original one stays at its initial place (so there'll be 2 buttons in the view). When the panning ends, the new button is used for some processing, and after that it should disappear (the original one stays as it is; so this whole process can repeat). Currently what I have is as below:
private func addPanGesture() {
for btn in self.selectors { //selectors is a list of buttons which needs this gesture
let pan = UIPanGestureRecognizer(target: self, action:#selector(self.panDetected(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
btn.addGesturerecognizer(pan)
}
}
#objc private func panDetected(_ panGesture: UIPanGestureRecognizer) {
var translation = panGesture.translation(in: view)
panGesture.setTranslation(CGPoint(x: 0, y: 0), in: view)
var newButton = UIButton()
if let initButton = panGesture.view as? UIButton {
print ("Button recognized!") // this msg is printed out
newButton.center = CGPoint(x: initButton.center.x + translation.x, y: initButton.center.y + translation.y)
newButton.setImage(UIImage(named: "somename"), for: .normal)
}
if panGesture.state == UIGestureRecognizerState.began {
self.view.addSubview(newButton)
}
if panGesture.state == UIGestureRecognizerState.ended {
//some other processing
}
if panGesture.state == UIGestureRecognizerState.changed {
self.view.addSubview(newButton)
}
// printed-out msgs show began, ended, changed states have all been reached
}
But the new button doesn't show up in my view. May I know how to solve this?
You need to create and add the new button as a subview only on .began and remove it on .ended.
Therefore you need to keep a reference to the new button.
You are setting the new button's center but not it's size. You might set its .frame.
You do not need to set a translation to the pan gesture. When you get var translation = panGesture.translation(in: view) you get everything you need.
I have wrote the below code for only one button, but if you are going to allow simultaneous dragging of buttons, you would need to keep a list of moving buttons instead of var movingButton: UIButton?
private func addPanGesture() {
let pan = UIPanGestureRecognizer(target: self, action:#selector(self.panDetected(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
btn.addGestureRecognizer(pan)
}
#objc private func panDetected(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
let initButton = panGesture.view as! UIButton
if panGesture.state == UIGestureRecognizerState.began {
// this is just copying initial button
// this might be overkill
// just make sure you set the frame, title and image of the new button correctly
let initButtonData = NSKeyedArchiver.archivedData(withRootObject: initButton)
let newButton = NSKeyedUnarchiver.unarchiveObject(with: initButtonData) as! UIButton
// we store new button's reference since we will just move it while it is added to view
movingButton = newButton
self.view.addSubview(movingButton!)
}
if panGesture.state == UIGestureRecognizerState.ended {
//some other processing
// when we are done just we just remove it from superview
movingButton!.removeFromSuperview()
}
if panGesture.state == UIGestureRecognizerState.changed {
// at any change, all we need to do is update movingButton's frame
var buttonFrame = initButton.frame;
buttonFrame.origin = CGPoint(x: buttonFrame.origin.x + translation.x, y: buttonFrame.origin.y + translation.y)
movingButton!.frame = buttonFrame
}
}
Hard to say without debugging it, but a few things I see:
You create a new button every time through panDetected, and add it to the view each time. You should only create an add the button in the .began state.
You should use init(frame:) to create your button, and initialize it to the size of the image.
It looks like you're attaching the pan gestures to the buttons. Then you get the pan coordinates in the button's coordinate system, which doesn't make sense. You should be converting the pan gesture to the button's superview's coordinate system, and should not be calling setTranslation except when the pan gesture's state is .began.
You should be setting the button's coordinates to the new location of the pan gesture each time you get a 1st.changed` message.
I am trying to pan 'blockView' with UIPanGestureRecognizer. When the pan is 'Ended', want the 'blockView' to decelerate with inertia, just as a UIScrollView would end scrolling with inertia.
override func viewDidLoad() {
super.viewDidLoad()
self.dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
let collider = UICollisionBehavior(items: [self.magnifiedView!])
collider.collisionDelegate = self
collider.collisionMode = .Boundaries
collider.translatesReferenceBoundsIntoBoundary = true
self.dynamicAnimator.addBehavior(collider)
self.dynamicProperties = UIDynamicItemBehavior(items: [self.magnifiedView!])
//self.dynamicProperties.elasticity = 1.0
//self.dynamicProperties.friction = 0.0
self.dynamicProperties.allowsRotation = false
//self.dynamicProperties.resistance = 10.0
self.dynamicAnimator.addBehavior(self.dynamicProperties)
}
func panBlockView (panGesture: UIPanGestureRecognizer) {
switch panGesture {
case .Began:
self.blockViewLocation = (self.blockView.center)!
case .Changed:
let translation = panGesture.translationInView(self.view)
self.blockView.center = CGPointMake(self.blockViewLocation.x + translation.x, self.self.blockViewLocation.y + translation.y)
case .Ended, .Cancelled:
self.dynamicAnimator.addLinearVelocity(panGesture.velocityInView(self.view), forItem: self.blockView)
}
}
Issue:
As I pan the view, 'blockView' tries to snap to the origin where the pan was originated from. When the pan 'Ends', it creates inertia, but it starts from the origin of the pan (after snapping to pan origin).
NOTE:
Above code works without issues just to PAN THE VIEW.
Adding a Snap Behavior and have it snap fixed the issue.