I am trying to move the bottom view up/ down using a pan gesture. It worked but has an issue of flickering and therefore it's not a smooth transition.
Here is the code
#IBOutlet weak var bottomViewHeight: NSLayoutConstraint!
#IBOutlet weak var bottomView: UIView!
var maxHeight: CGFloat = 297
var minHeight: CGFloat = 128
let panGest = PanVerticalScrolling(target: self, action: #selector(panAction(sender:)))
bottomView.addGestureRecognizer(panGest)
#objc func panAction(sender: UIPanGestureRecognizer) {
if sender.state == .changed {
let endPosition = sender.location(in: self.bottomView)
let differenceHeight = maxHeight - endPosition.y
if differenceHeight < maxHeight && differenceHeight > minHeight {
bottomViewHeight.constant = differenceHeight
self.bottomView.layoutIfNeeded()
}
}
}
Here is the gesture class
class PanVerticalScrolling : UIPanGestureRecognizer {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if state == .began {
let vel = velocity(in: self.view)
if abs(vel.x) > abs(vel.y) {
state = .cancelled
}
}
}
}
And here in the image, you can check the actual issue
The problem is that you are repeatedly getting the touch position in the bottom view ... and then changing bottom view size.
Since the touch Y is relative to the height of the view, it's bouncing around.
Try this...
Add a class property:
var initialY: CGFloat = -1.0
and change your panAction() to this:
#objc func panAction(sender: UIPanGestureRecognizer) {
if sender.state == .changed {
if initialY == -1.0 {
initialY = sender.location(in: self.view).y
}
let endPositionY = sender.location(in: self.view).y - initialY
let differenceHeight = maxHeight - endPositionY
if differenceHeight < maxHeight && differenceHeight > minHeight {
bottomViewHeight.constant = differenceHeight
self.bottomView.layoutIfNeeded()
}
}
}
You'll need to reset initialY to -1 each time you "end" the drag / resize process.
Edit
A better way - maintains current state:
// these will be used inside panAction()
var initialY: CGFloat = -1.0
var initialHeight: CGFloat = -1.0
#objc func panAction(sender: UIPanGestureRecognizer) {
if sender.state == .changed {
let endPositionY = sender.location(in: self.view).y - initialY
let differenceHeight = self.initialHeight - endPositionY
if differenceHeight < maxHeight && differenceHeight > minHeight {
bottomViewHeight.constant = differenceHeight
}
}
if sender.state == .began {
// reset position and height tracking properties
self.initialY = sender.location(in: self.view).y
self.initialHeight = bottomViewHeight.constant
}
}
Related
How would you go about dragging up a tab at the bottom of the screen like in apple maps and be able to drag it above the parent UIView, but keep it constrained within bounds of the screen.
i got this code which I'm trying to modify for my use case but I'm struggling to set the right bounds so it cant move up past a certain point and move down past a certain point.
var originalX: CGFloat = 0.0
var originalY: CGFloat = 0.0
#objc func handlePanGesture(gesture: UIPanGestureRecognizer) {
guard let panView = gesture.view else {
return
}
self.view.bringSubviewToFront(panView)
var translatedPoint = gesture.translation(in: self.view)
if gesture.state == .began {
originalX = panView.center.x
originalY = panView.center.y
}
translatedPoint = CGPoint(x: originalX, y: originalY + translatedPoint.y)
print(originalY + translatedPoint.y)
print(view.frame.height)
if (originalY + translatedPoint.y) > view.frame.height {
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut], animations: {
gesture.view?.center = translatedPoint
} , completion: nil)
}
}
new code with offset
var originalX: CGFloat = 0.0
var originalY: CGFloat = 0.0
var currentPoint: CGPoint?
var position:CGPoint?
#objc func handlePanGesture(gesture: UIPanGestureRecognizer) {
guard let panView = gesture.view else {
return
}
self.view.bringSubviewToFront(panView)
var translatedPoint = gesture.translation(in: self.view)
if gesture.state == .began {
originalX = panView.center.x
originalY = panView.center.y
}
translatedPoint = CGPoint(x: originalX, y: originalY + translatedPoint.y)
//if offsetY >= 9 {
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut], animations: {
gesture.view?.center = translatedPoint
} , completion: nil)
//}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
position = touch.location(in: view)
//print("position\(position)")
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
currentPoint = touch.location(in: view)
//print("currentPoint\(currentPoint)")
let offsetY = currentPoint!.y - position!.y
print(offsetY)
}
}
out put when dragging container view
2.0
0.5
9.0
1.0
8.0
0.5
4.0
7.5
-0.5
-6.5
-1.0
-8.0
output for dragging on uiview
-11.5
-21.5
-36.0
-52.0
-74.5
-86.5
-98.0
-102.0
-106.0
-112.0
-116.5
-120.0
-122.5
-124.0
-124.0
-124.0
-124.0
-124.0
-123.5
-120.5
-116.5
-114.0
-109.0
-104.5
-97.0
-89.0
-80.0
-74.0
-66.0
-58.5
-54.5
-49.5
-47.0
-42.5
-39.5
-35.5
-31.0
-28.5
-24.0
-21.5
-19.5
-18.0
-16.0
-15.5
-15.0
You can try that, and record the TouchPoint at TouchBegin(beginDrag) and get currentPoint at TouchMove(dragging)——I don't remember the specific agency method. By calculating offsetX = currentpoint.x - touchpoint.x. Using gestures to calculate is too complicated。
offsetX = currentpoint.x - touchpoint.x;
offsetY = currentpoint.y - touchpoint.y;
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 want to drag and zoom UIImageView inside UIView not in UIViewController (self.view) using swift 3.0.
Here I have declare mainView and imageView,
var lastScale: CGFloat = 0.0
#IBOutlet var mainView: UIView!
#IBOutlet var imageView: UIImageView!
I have set Pan and Pinch Gesture for zoom and drag imageview inside mainView
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(ViewController.handlePinchGesture(_:)))
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(pinchGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePanGesture(recognizer:)))
imageView.addGestureRecognizer(panGestureRecognizer)
Function for pan and pinch gesture
func handlePinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began {
lastScale = gestureRecognizer.scale
}
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let currentScale: CGFloat = (gestureRecognizer.view!.layer.value(forKeyPath: "transform.scale")! as! CGFloat)
// Constants to adjust the max/min values of zoom
//let kMaxScale: CGFloat = 2.0
let kMinScale: CGFloat = 1.0
var newScale: CGFloat = 1 - (lastScale - gestureRecognizer.scale)
//newScale = min(newScale, kMaxScale / currentScale)
newScale = max(newScale, kMinScale / currentScale)
let transform = gestureRecognizer.view?.transform.scaledBy(x: newScale, y: newScale)
gestureRecognizer.view?.transform = transform!
lastScale = gestureRecognizer.scale
}
}
func handlePanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: mainView)
var recognizerFrame = recognizer.view?.frame
recognizerFrame?.origin.x += translation.x
recognizerFrame?.origin.y += translation.y
// Check if UIImageView is completely inside its superView
if mainView.bounds.contains(recognizerFrame!) {
recognizer.view?.frame = recognizerFrame!
}
else {
// Check vertically
if (recognizerFrame?.origin.y)! < mainView.bounds.origin.y {
recognizerFrame?.origin.y = 0
}
else if (recognizerFrame?.origin.y)! + (recognizerFrame?.size.height)! > mainView.bounds.size.height {
recognizerFrame?.origin.y = mainView.bounds.size.height - (recognizerFrame?.size.height)!
}
// Check horizantally
if (recognizerFrame?.origin.x)! < mainView.bounds.origin.x {
recognizerFrame?.origin.x = 0
}
else if (recognizerFrame?.origin.x)! + (recognizerFrame?.size.width)! > mainView.bounds.size.width {
recognizerFrame?.origin.x = mainView.bounds.size.width - (recognizerFrame?.size.width)!
}
}
// Reset translation so that on next pan recognition
// we get correct translation value
recognizer.setTranslation(CGPoint(x: CGFloat(0), y: CGFloat(0)), in: mainView)
}
When I run this code, And I'm zoom imageView it's go outside of mainView.
So, How can I fixed?
Whenever you scale your subview or move your subview, the frame value of subview needs to be changed. Because frame is a property of UIView, you can use KVO to observe it and set some rules for its changes like your way done for pan etc.
For KVO of frame, you can find better explanation from here
I asked a question on stackoverflow and received a perfect answer to my case.
To get the start point and end point of UIPanGestureRecognizer, use the code:
var view = UIView()
func panGestureMoveAround(gesture: UIPanGestureRecognizer) {
var locationOfBeganTap: CGPoint
var locationOfEndTap: CGPoint
if gesture.state == UIGestureRecognizerState.Began {
locationOfBeganTap = gesture.locationInView(view)
} else if gesture.state == UIGestureRecognizerState.Ended {
locationOfEndTap = gesture.locationInView(view)
}
}
And for better viewing, I would draw a rectangle that caught the starting point and auto-resize to complete the movement.
How could I draw a rectangle the starting point and follow my movement to end the gesture?
You need to create a subclass of UIView call it something like RectView and like this:
#IBDesignable
class RectView: UIView {
#IBInspectable var startPoint:CGPoint?{
didSet{
self.setNeedsDisplay()
}
}
#IBInspectable var endPoint:CGPoint?{
didSet{
self.setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
if (startPoint != nil && endPoint != nil){
let path:UIBezierPath = UIBezierPath(rect: CGRectMake(min(startPoint!.x, endPoint!.x),
min(startPoint!.y, endPoint!.y),
fabs(startPoint!.x - endPoint!.x),
fabs(startPoint!.y - endPoint!.y)))
path.stroke()
}
}
}
Then in your view controller you can say something like:
#IBAction func panGestureMoveAround(sender: UIPanGestureRecognizer) {
var locationOfBeganTap: CGPoint
var locationOfEndTap: CGPoint
if sender.state == UIGestureRecognizerState.Began {
locationOfBeganTap = sender.locationInView(rectView)
rectView.startPoint = locationOfBeganTap
rectView.endPoint = locationOfBeganTap
} else if sender.state == UIGestureRecognizerState.Ended {
locationOfEndTap = sender.locationInView(rectView)
rectView.endPoint = sender.locationInView(rectView)
} else{
rectView.endPoint = sender.locationInView(rectView)
}
}
Swift 5 version
class RectView: UIView {
var startPoint: CGPoint? {
didSet {
setNeedsDisplay()
}
}
var endPoint: CGPoint? {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
guard let startPoint = startPoint, let endPoint = endPoint else {
return
}
let path = UIBezierPath(
rect: CGRect(
x: min(startPoint.x, endPoint.x),
y: min(startPoint.y, endPoint.y),
width: abs(startPoint.x - endPoint.x),
height: abs(startPoint.y - endPoint.y)
)
)
path.stroke()
}
}
#objc func panGestureMoveAround(_ sender: UIPanGestureRecognizer) {
var locationOfBeganTap: CGPoint
switch sender.state {
case .began:
locationOfBeganTap = sender.location(in: rectView)
rectView.startPoint = locationOfBeganTap
rectView.endPoint = locationOfBeganTap
case .changed:
rectView.endPoint = sender.location(in: rectView)
case .ended:
rectView.endPoint = sender.location(in: rectView)
default:
break
}
}
I understand that the title is a bit confusing so I'll try my best to explain it well. I have a UIView that has a fixed width of 100, and a variable height that starts at 0. What I want to be able to do is drag my finger from the base of the UIView, towards the top of the screen, and the UIView changes its height/extends to the position of my finger. If that's still to complicated just imagine it as a strip of paper being pulled out from under something.
If you can help, that would be really great. I don't think it should be too hard, but I'm only a beginner and I can understand if I haven't explained it well!
This should be straight-forward with a UIPanGestureRecognizer and a little math. To change the view to the correct frame (I'm using the name _viewToChange, so replace that with your view later), simply add:
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1;
[self addGestureRecognizer:pan];
to your init method for the super view, and define the method:
- (void)pan:(UIPanGestureRecognizer *)aPan; {
CGPoint currentPoint = [aPan locationInView:self];
[UIView animateWithDuration:0.01f
animations:^{
CGRect oldFrame = _viewToChange.frame;
_viewToChange.frame = CGRectMake(oldFrame.origin.x, currentPoint.y, oldFrame.size.width, ([UIScreen mainScreen].bounds.size.height - currentPoint.y));
}];
}
This should animate the view up as the users finger moves. Hope that Helps!
Swift version using UIPanGestureRecognizer
Using PanGesture added to UIView from Storyboard and delegate set to self.
UIView is attached to bottom of the screen.
class ViewController: UIViewController {
var translation: CGPoint!
var startPosition: CGPoint! //Start position for the gesture transition
var originalHeight: CGFloat = 0 // Initial Height for the UIView
var difference: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
originalHeight = dragView.frame.height
}
#IBOutlet weak var dragView: UIView! // UIView with UIPanGestureRecognizer
#IBOutlet var gestureRecognizer: UIPanGestureRecognizer!
#IBAction func viewDidDragged(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
startPosition = gestureRecognizer.location(in: dragView) // the postion at which PanGestue Started
}
if sender.state == .began || sender.state == .changed {
translation = sender.translation(in: self.view)
sender.setTranslation(CGPoint(x: 0.0, y: 0.0), in: self.view)
let endPosition = sender.location(in: dragView) // the posiion at which PanGesture Ended
difference = endPosition.y - startPosition.y
var newFrame = dragView.frame
newFrame.origin.x = dragView.frame.origin.x
newFrame.origin.y = dragView.frame.origin.y + difference //Gesture Moving Upward will produce a negative value for difference
newFrame.size.width = dragView.frame.size.width
newFrame.size.height = dragView.frame.size.height - difference //Gesture Moving Upward will produce a negative value for difference
dragView.frame = newFrame
}
if sender.state == .ended || sender.state == .cancelled {
//Do Something
}
}
}
This is my swift solution in doing it using autolayout and a height constraint on the view you want to resize, while setting a top and bottom limit based on the safe areas
private var startPosition: CGPoint!
private var originalHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewDidDrag(_:)))
resizableView.addGestureRecognizer(panGesture)
}
#objc private func viewDidDrag(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
startPosition = sender.location(in: self.view)
originalHeight = detailsContainerViewHeight.constant
}
if sender.state == .changed {
let endPosition = sender.location(in: self.view)
let difference = endPosition.y - startPosition.y
var newHeight = originalHeight - difference
if view.bounds.height - newHeight < topSafeArea + backButtonSafeArea {
newHeight = view.bounds.height - topSafeArea - backButtonSafeArea
} else if newHeight < routeDetailsHeaderView.bounds.height + bottomSafeArea {
newHeight = routeDetailsHeaderView.bounds.height
}
resizableView.constant = newHeight
}
if sender.state == .ended || sender.state == .cancelled {
//Do Something if interested when dragging ended.
}
}