adding inertia to a UIPanGestureRecognizer - ios

I am trying to move a sub view across the screen which works, but i also want to add inertia or momentum to the object.
My UIPanGestureRecognizer code that i already have is below.
Thanks in advance.
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self addGestureRecognizer:panGesture];
(void)handlePan:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:self.superview];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.superview];
if (recognizer.state == UIGestureRecognizerStateEnded) {
[self.delegate card:self.tag movedTo:self.frame.origin];
}
}
Again thanks.

Have a look at RotationWheelAndDecelerationBehaviour. there is an example for how to do the deceleration for both linear panning and rotational movement. Trick is to see what is the velocity when user ends the touch and continue in that direction with a small deceleration.

Well, I'm not a pro but, checking multiple answers, I managed to make my own code with which I am happy.
Please tell me how to improve it and if there are any bad practices I used.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translatedPoint = [recognizer translationInView:self.postViewContainer];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
float bottomMargin = self.view.frame.size.height - containerViewHeight;
float topMargin = self.view.frame.size.height - scrollViewHeight;
if ([recognizer state] == UIGestureRecognizerStateChanged) {
newYOrigin = self.postViewContainer.frame.origin.y + translatedPoint.y;
if (newYOrigin <= bottomMargin && newYOrigin >= topMargin) {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, self.postViewContainer.center.y + translatedPoint.y);
}
[recognizer setTranslation:CGPointMake(0, 0) inView:self.postViewContainer];
}
if ([recognizer state] == UIGestureRecognizerStateEnded) {
__block float newYAnimatedOrigin = self.postViewContainer.frame.origin.y + (velocity.y / 2.5);
if (newYAnimatedOrigin <= bottomMargin && newYAnimatedOrigin >= topMargin) {
[UIView animateWithDuration:1.2 delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, self.postViewContainer.center.y + (velocity.y / 2.5));
}
completion:^(BOOL finished) {
[self.postViewContainer setFrame:CGRectMake(0, newYAnimatedOrigin, self.view.frame.size.width, self.view.frame.size.height - newYAnimatedOrigin)];
}
];
}
else {
[UIView animateWithDuration:0.6 delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
if (newYAnimatedOrigin > bottomMargin) {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, bottomMargin + self.postViewContainer.frame.size.height / 2);
}
if (newYAnimatedOrigin < topMargin) {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, topMargin + self.postViewContainer.frame.size.height / 2);
}
}
completion:^(BOOL finished) {
if (newYAnimatedOrigin > bottomMargin)
[self.postViewContainer setFrame:CGRectMake(0, bottomMargin, self.view.frame.size.width, scrollViewHeight)];
if (newYAnimatedOrigin < topMargin)
[self.postViewContainer setFrame:CGRectMake(0, topMargin, self.view.frame.size.width, scrollViewHeight)];
}
];
}
}
}
I have used two different animation, one is the default inertia one and the other if for when the user flings the containerView with high velocity.
It works well under iOS 7.

I took the inspiration from the accepted answer's implementation. Here is a Swift 5.1 version.
Logic:
You need to calculate the angle changes with the velocity at which the pan gesture ended and keep rotating the wheel in an endless timer until the velocity wears down because of deceleration rate.
Keep decreasing the current velocity in every iteration of the timer
with some factor (say, 0.9).
Keep a lower limit on the velocity to
invalidate the timer and complete the deceleration process.
Main function used to calculate deceleration:
// deceleration behaviour constants (change these for different deceleration rates)
private let timerDuration = 0.025
private let decelerationSmoothness = 0.9
private let velocityToAngleConversion = 0.0025
private func animateWithInertia(velocity: Double) {
_ = Timer.scheduledTimer(withTimeInterval: self.timerDuration, repeats: true) { [weak self] timer in
guard let this = self else {
return
}
let concernedVelocity = this.currentVelocity == 0.0 ? velocity : this.currentVelocity
let newVelocity = concernedVelocity * this.decelerationSmoothness
this.currentVelocity = newVelocity
var angleTraversed = newVelocity * this.velocityToAngleConversion * this.maximumRotationAngleInCircle
if !this.isClockwiseRotation {
angleTraversed *= -1
}
// exit condition
if newVelocity < 0.1 {
timer.invalidate()
this.currentVelocity = 0.0
} else {
this.traverseAngularDistance(angle: angleTraversed)
}
}
}
Full working code with helper functions, extensions and usage of aforementioned function in the handlePanGesture function.
// deceleration behaviour constants (change these for different deceleration rates)
private let timerDuration = 0.025
private let decelerationSmoothness = 0.9
private let velocityToAngleConversion = 0.0025
private let maximumRotationAngleInCircle = 360.0
private var currentRotationDegrees: Double = 0.0 {
didSet {
if self.currentRotationDegrees > self.maximumRotationAngleInCircle {
self.currentRotationDegrees = 0
}
if self.currentRotationDegrees < -self.maximumRotationAngleInCircle {
self.currentRotationDegrees = 0
}
}
}
private var previousLocation = CGPoint.zero
private var currentLocation = CGPoint.zero
private var velocity: Double {
let xFactor = self.currentLocation.x - self.previousLocation.x
let yFactor = self.currentLocation.y - self.previousLocation.y
return Double(sqrt((xFactor * xFactor) + (yFactor * yFactor)))
}
private var currentVelocity = 0.0
private var isClockwiseRotation = false
#objc private func handlePanGesture(panGesture: UIPanGestureRecognizer) {
let location = panGesture.location(in: self)
if let rotation = panGesture.rotation {
self.isClockwiseRotation = rotation > 0
let angle = Double(rotation).degrees
self.currentRotationDegrees += angle
self.rotate(angle: angle)
}
switch panGesture.state {
case .began, .changed:
self.previousLocation = location
case .ended:
self.currentLocation = location
self.animateWithInertia(velocity: self.velocity)
default:
print("Fatal State")
}
}
private func animateWithInertia(velocity: Double) {
_ = Timer.scheduledTimer(withTimeInterval: self.timerDuration, repeats: true) { [weak self] timer in
guard let this = self else {
return
}
let concernedVelocity = this.currentVelocity == 0.0 ? velocity : this.currentVelocity
let newVelocity = concernedVelocity * this.decelerationSmoothness
this.currentVelocity = newVelocity
var angleTraversed = newVelocity * this.velocityToAngleConversion * this.maximumRotationAngleInCircle
if !this.isClockwiseRotation {
angleTraversed *= -1
}
if newVelocity < 0.1 {
timer.invalidate()
this.currentVelocity = 0.0
this.selectAtIndexPath(indexPath: this.nearestIndexPath, shouldTransformToIdentity: true)
} else {
this.traverseAngularDistance(angle: angleTraversed)
}
}
}
private func traverseAngularDistance(angle: Double) {
// keep the angle in -360.0 to 360.0 range
let times = Double(Int(angle / self.maximumRotationAngleInCircle))
var newAngle = angle - times * self.maximumRotationAngleInCircle
if newAngle < -self.maximumRotationAngleInCircle {
newAngle += self.maximumRotationAngleInCircle
}
self.currentRotationDegrees += newAngle
self.rotate(angle: newAngle)
}
Extensions being used in above code:
extension UIView {
func rotate(angle: Double) {
self.transform = self.transform.rotated(by: CGFloat(angle.radians))
}
}
extension Double {
var radians: Double {
return (self * Double.pi)/180
}
var degrees: Double {
return (self * 180)/Double.pi
}
}

Related

UIImageView disappears after rotate and move to edge

I have written a custom class which offers the feature to move and rotates images.
I need to restrict the movement to the boundaries of parent view or Superview.
So, I wrote below code to restrict it.
This works fine before an image is rotated. If I try to rotate the image and then move the image to an edge, Image disappears leaving no log or traces.
Why does it disappear and how do I avoid it?
if(frame.origin.x < 1)
{
frame.origin.x = 1
}
if(frame.origin.y < 1)
{
frame.origin.y = 1
}
if(frame.maxX > superview!.frame.width)
{
frame.origin.x = superview!.frame.width - frame.width - 1
}
if(frame.maxY > superview!.frame.height)
{
frame.origin.y = superview!.frame.height - frame.height - 1
}
If I remove the above code, nothing disappears but image moves out of boundaries. So I feel something wrong in only above lines.
So help me to correctly implement this feature after rotation.
Full Movable Image Class code :
class movableImageView: UIImageView
{
var CenCooVar = CGPoint()
override init(image: UIImage!)
{
super.init(image: image)
self.userInteractionEnabled = true
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(moveImageFnc(_:)))
let rotateImage = UIRotationGestureRecognizer(target: self, action: #selector(rotateImageFnc(_:)))
self.gestureRecognizers = [moveImage,rotateImage]
}
func moveImageFnc(moveImage: UIPanGestureRecognizer)
{
if moveImage.state == UIGestureRecognizerState.Began
{
CenCooVar = self.center
}
if moveImage.state == UIGestureRecognizerState.Changed
{
let moveCooVar = moveImage.translationInView(self.superview!)
self.center = CGPoint(x: CenCooVar.x + moveCooVar.x, y: CenCooVar.y + moveCooVar.y)
if(frame.origin.x < 1)
{
frame.origin.x = 1
}
if(frame.origin.y < 1)
{
frame.origin.y = 1
}
if(frame.maxX > superview!.frame.width)
{
frame.origin.x = superview!.frame.width - frame.width - 1
}
if(frame.maxY > superview!.frame.height)
{
frame.origin.y = superview!.frame.height - frame.height - 1
}
}
if moveImage.state == UIGestureRecognizerState.Ended
{
CenCooVar = self.center
}
}
func rotateImageFnc(rotateImage: UIRotationGestureRecognizer)
{
if rotateImage.state == UIGestureRecognizerState.Changed
{
self.transform = CGAffineTransformRotate(self.transform, rotateImage.rotation)
rotateImage.rotation = 0
}
}
}

How to move an image within a view

Trying to move an image within a subview by passing certain values in the app.How to calculate the pixel points "CGPoints" of the view.What will be the best way to this.
setFrame:CGRectMake(_imgstride.frame.origin.x+_imgstride.frame.size.width,
_imgstride.frame.origin.y, _imgstride.frame.size.width,
_imgstride.frame.size.height)];
I tried with the above code, the imageView is moving but not working the way i want.
Try This.
For Swift :
Its Work For me.imageview move in view.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let myPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(myPanAction))
myPanGestureRecognizer.minimumNumberOfTouches = 1
myPanGestureRecognizer.maximumNumberOfTouches = 1
self.imageView.addGestureRecognizer(myPanGestureRecognizer)
}
func myPanAction(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
var recognizerFrame = recognizer.view?.frame
recognizerFrame?.origin.x += translation.x
recognizerFrame?.origin.y += translation.y
if (self.view.bounds.contains(recognizerFrame!)) {
recognizer.view?.frame = recognizerFrame!
}else{
if (recognizerFrame?.origin.y)! < self.view.bounds.origin.y {
recognizerFrame?.origin.y = 0
}else if ((recognizerFrame?.origin.y)! + (recognizerFrame?.size.height)! > self.view.bounds.size.height) {
recognizerFrame?.origin.y = self.view.bounds.size.height - (recognizerFrame?.size.height)!
}
if (recognizerFrame?.origin.x)! < self.view.bounds.origin.x {
recognizerFrame?.origin.x = 0
}else if ((recognizerFrame?.origin.x)! + (recognizerFrame?.size.width)! > self.view.bounds.size.width) {
recognizerFrame?.origin.x = self.view.bounds.size.width - (recognizerFrame?.size.width)!
}
}
recognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
For Objective C :
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.imageview addGestureRecognizer:pan];
}
#pragma mark - Gesture Recognizer
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
CGPoint translation = [gesture translationInView:self.view];
CGRect recognizerFrame = gesture.view.frame;
recognizerFrame.origin.x += translation.x;
recognizerFrame.origin.y += translation.y;
if (CGRectContainsRect(self.view.bounds, recognizerFrame)) {
gesture.view.frame = recognizerFrame;
}else {
if (recognizerFrame.origin.y < self.view.bounds.origin.y) {
recognizerFrame.origin.y = 0;
}else if (recognizerFrame.origin.y + recognizerFrame.size.height > self.view.bounds.size.height) {
recognizerFrame.origin.y = self.view.bounds.size.height - recognizerFrame.size.height;
}
if (recognizerFrame.origin.x < self.view.bounds.origin.x) {
recognizerFrame.origin.x = 0;
}else if (recognizerFrame.origin.x + recognizerFrame.size.width > self.view.bounds.size.width) {
recognizerFrame.origin.x = self.view.bounds.size.width - recognizerFrame.size.width;
}
}
[gesture setTranslation:CGPointMake(0, 0) inView:self.view];
}
#end

UIPanGesture not moving individual view

I am doing a iPhone project in Swift where on top of my homeScreen, I am adding multiple views in a stack.
My requirement is to be able to move each individual child view on top of the homeScreen like Tinder card swiping effect. I am using UIPanGesture to achieve this so that the each individual view follows my finger on screen.
But my problem is that instead of the desired effect of moving only one screen all the stack of Views is moving together on my Home Screen.
I am stuck at this problem for the last 4 days. Kindly help me out.
Xcode Version 7.2.1 and Swift 2.1
Here is the code that moves the UIView:
//For creating each childView and adding UIPanGestureRecognizer to each childView
func configureInitialViewPlacement() -> Void {
for var i:Int = cardsArray.count-1; 0 <= i; i--
{
let cardView = cardsArray[i]
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: "beingDragged:")
cardView.addGestureRecognizer(panGestureRecognizer)
self.view.addSubview(cardView)
var frame = CGRectZero
var originalFrame = self.view.bounds
originalFrame = CGRectMake(originalFrame.origin.x+10,originalFrame.origin.y+10 , originalFrame.size.width-20, originalFrame.size.height-20)
frame.size.height = originalFrame.size.height
frame.size.width = originalFrame.size.width - CGFloat((2*CGFloat(i)*paddingOffset))
frame.origin.x = originalFrame.origin.x + CGFloat((CGFloat(i)*paddingOffset))
frame.origin.y = originalFrame.origin.y + CGFloat((CGFloat(i)*paddingOffset))
cardView.frame = frame
cardView.setContentViewForCard(cardDataArray[i])
}
}
// Method For gestureRecognizer
func beingDragged(gestureRecognizer: UIPanGestureRecognizer) -> Void {
xFromCenter = Float(gestureRecognizer.translationInView(self.view).x)
yFromCenter = Float(gestureRecognizer.translationInView(self.view).y)
switch gestureRecognizer.state {
case UIGestureRecognizerState.Began:
self.originPoint = self.view.center
case UIGestureRecognizerState.Changed:
let rotationStrength: Float = min(xFromCenter/ROTATION_STRENGTH, ROTATION_MAX)
let rotationAngle = ROTATION_ANGLE * rotationStrength
let scale = max(1 - fabsf(rotationStrength) / SCALE_STRENGTH, SCALE_MAX)
self.view.center = CGPointMake(self.originPoint.x + CGFloat(xFromCenter), self.originPoint.y + CGFloat(yFromCenter))
let transform = CGAffineTransformMakeRotation(CGFloat(rotationAngle))
let scaleTransform = CGAffineTransformScale(transform, CGFloat(scale), CGFloat(scale))
self.view.transform = scaleTransform
self.updateOverlay(CGFloat(xFromCenter))
case UIGestureRecognizerState.Ended:
self.afterSwipeAction()
case UIGestureRecognizerState.Possible:
fallthrough
case UIGestureRecognizerState.Cancelled:
fallthrough
case UIGestureRecognizerState.Failed:
fallthrough
default:
break
}
}
func afterSwipeAction() -> Void {
let floatXFromCenter = Float(xFromCenter)
if floatXFromCenter > ACTION_MARGIN {
self.rightAction()
} else if floatXFromCenter < -ACTION_MARGIN {
self.leftAction()
} else {
UIView.animateWithDuration(0.3, animations: {() -> Void in
self.view.center = self.originPoint
self.view.transform = CGAffineTransformMakeRotation(0)
})
}
}
// For Right Swipe
func rightAction() -> Void {
let finishPoint: CGPoint = CGPointMake(500, 2 * CGFloat(yFromCenter) + self.originPoint.y)
UIView.animateWithDuration(0.3,
animations: {
self.view.center = finishPoint
}, completion: {
(value: Bool) in
self.cardsArray[0].removeFromSuperview()
})
delegateforcard.cardSwipedRight(self.cardsArray[0])
}
// For Left Swipe
func leftAction() -> Void {
let finishPoint: CGPoint = CGPointMake(-500, 2 * CGFloat(yFromCenter) + self.originPoint.y)
UIView.animateWithDuration(0.3,
animations: {
self.view.center = finishPoint
}, completion: {
(value: Bool) in
self.cardsArray[0].removeFromSuperview()
})
delegateforcard.cardSwipedLeft(self.cardsArray[0])
}
Let me know if you need more clarifications.
Thanks.
I have also done the same in objective-c, You can get an idea from here,
Here is my code
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(cardMoving:)];
-(void)cardMoving:(UIPanGestureRecognizer *)gestureRecognizer
{
xValueFromCenter = [gestureRecognizer translationInView:self].x; // if right positive(+) value, negative for left
yValueFromCenter = [gestureRecognizer translationInView:self].y; // if swipe up positive(+), negative for down
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
originalPoint = self.center;
break;
};
case UIGestureRecognizerStateChanged:{
CGFloat rotationStrength = MIN(xValueFromCenter / ROTATION_STRENGTH, ROTATION_MAX);
CGFloat rotationAngel = (CGFloat) (ROTATION_ANGLE * rotationStrength);
CGFloat scale = MAX(1 - fabs(rotationStrength) / SCALE_STRENGTH, SCALE_MAX);
self.center = CGPointMake(originalPoint.x + xValueFromCenter, originalPoint.y + yValueFromCenter);
CGAffineTransform transform = CGAffineTransformMakeRotation(rotationAngel);
CGAffineTransform scaleTransform = CGAffineTransformScale(transform, scale, scale);
self.transform = scaleTransform;
[self updateOverlay:xValueFromCenter];
break;
};
case UIGestureRecognizerStateEnded: {
[self afterSwipeAction];
break;
};
case UIGestureRecognizerStatePossible:break;
case UIGestureRecognizerStateCancelled:break;
case UIGestureRecognizerStateFailed:break;
}
}
-(void)updateOverlay:(CGFloat)distance
{
if (distance > 0) {
overlayView.Direction = GGOverlayViewDirectionRight;
} else {
overlayView.Direction = GGOverlayViewDirectionLeft;
}
overlayView.alpha = MIN(fabs(distance)/100, 0.7);
}
- (void)afterSwipeAction
{
if (xValueFromCenter > ACTION_MARGIN) {
[self rightAction];
} else if (xValueFromCenter < -ACTION_MARGIN) {
[self leftAction];
} else { //for reseting the card
[UIView animateWithDuration:0.3
animations:^{
self.center = originalPoint;
self.transform = CGAffineTransformMakeRotation(0);
overlayView.alpha = 0;
}];
}
}

How to enable "tap and slide" in a UISlider?

What I want to get is a UISlider which lets the user not only slide when he starts on its thumbRect, but also when he taps elsewhere. When the user taps on the slider but outside of the thumbRect, the slider should jump to that value and then still keeping up to the user's sliding gesture.
What I have tried so far was implementing a subclass of UIGestureRecognizer like in this suggestion. It starts right then when a touch down somewhere outside the thumbRect occurs. The problem is that the slider sets its value but then further sliding gestures are ignored because the touch down recognizer has stolen the touch.
How can I implement a slider where you can tap anywhere but still slide right away?
Edit: ali59a was so kind to add an example of what I've done now. This requires to lift the finger again, after that I can touch and drag to slide (a tap is not what I want, I need a 'touch and slide' right away).
I'm not sure if you are still looking for an answer for this, but I was just looking at this myself today; and I managed to get it to work for me.
The key to it, is using a UILongPressGestureRecognizer instead of just a UITapGestureRecognizer, we can then set the minimumPressDuration of the recognizer to 0; making it act as a tap recognizer, except you can now actually check its state.
Putting what ali59a suggested will work for you, just by replacing the UITapGestureRecognizer with a UILongPressGestureRecognizer. However, I found that this didn't seem to quite put the thumbRect directly under my thumb. It appeared a bit off to me.
I created my own UISlider subclass for my project, and here is how I implemented the "tap and slide feature" for me.
In my init method:
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:#selector(tapAndSlide:)];
longPress.minimumPressDuration = 0;
[self addGestureRecognizer:longPress];
Then my tapAndSlide: method:
- (void)tapAndSlide:(UILongPressGestureRecognizer*)gesture
{
CGPoint pt = [gesture locationInView: self];
CGFloat thumbWidth = [self thumbRect].size.width;
CGFloat value;
if(pt.x <= [self thumbRect].size.width/2.0)
value = self.minimumValue;
else if(pt.x >= self.bounds.size.width - thumbWidth/2.0)
value = self.maximumValue;
else {
CGFloat percentage = (pt.x - thumbWidth/2.0)/(self.bounds.size.width - thumbWidth);
CGFloat delta = percentage * (self.maximumValue - self.minimumValue);
value = self.minimumValue + delta;
}
if(gesture.state == UIGestureRecognizerStateBegan){
[UIView animateWithDuration:0.35 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[self setValue:value animated:YES];
[super sendActionsForControlEvents:UIControlEventValueChanged];
} completion:nil];
}
else [self setValue:value];
if(gesture.state == UIGestureRecognizerStateChanged)
[super sendActionsForControlEvents:UIControlEventValueChanged];
}
Where I also used a method to return the frame of my custom thumbRect:
- (CGRect)thumbRect {
CGRect trackRect = [self trackRectForBounds:self.bounds];
return [self thumbRectForBounds:self.bounds trackRect:trackRect value:self.value];
}
I also have my slider animate to the position where the user first taps, over 0.35 seconds. Which I reckon looks pretty sweet, so I included that in that code.
If you don't want that, simply try this:
- (void)tapAndSlide:(UILongPressGestureRecognizer*)gesture
{
CGPoint pt = [gesture locationInView: self];
CGFloat thumbWidth = [self thumbRect].size.width;
CGFloat value;
if(pt.x <= [self thumbRect].size.width/2.0)
value = self.minimumValue;
else if(pt.x >= self.bounds.size.width - thumbWidth/2.0)
value = self.maximumValue;
else {
CGFloat percentage = (pt.x - thumbWidth/2.0)/(self.bounds.size.width - thumbWidth);
CGFloat delta = percentage * (self.maximumValue - self.minimumValue);
value = self.minimumValue + delta;
}
[self setValue:value];
if(gesture.state == UIGestureRecognizerStateChanged)
[super sendActionsForControlEvents:UIControlEventValueChanged];
}
I hope that makes sense, and helps you.
I converted the answer provided by DWilliames to Swift
Inside your viewDidAppear()
let longPress = UILongPressGestureRecognizer(target: self.slider, action: Selector("tapAndSlide:"))
longPress.minimumPressDuration = 0
self.addGestureRecognizer(longPress)
Class file
class TapUISlider: UISlider
{
func tapAndSlide(gesture: UILongPressGestureRecognizer)
{
let pt = gesture.locationInView(self)
let thumbWidth = self.thumbRect().size.width
var value: Float = 0
if (pt.x <= self.thumbRect().size.width / 2)
{
value = self.minimumValue
}
else if (pt.x >= self.bounds.size.width - thumbWidth / 2)
{
value = self.maximumValue
}
else
{
let percentage = Float((pt.x - thumbWidth / 2) / (self.bounds.size.width - thumbWidth))
let delta = percentage * (self.maximumValue - self.minimumValue)
value = self.minimumValue + delta
}
if (gesture.state == UIGestureRecognizerState.Began)
{
UIView.animateWithDuration(0.35, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut,
animations:
{
self.setValue(value, animated: true)
super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
},
completion: nil)
}
else
{
self.setValue(value, animated: false)
}
}
func thumbRect() -> CGRect
{
return self.thumbRectForBounds(self.bounds, trackRect: self.bounds, value: self.value)
}
}
You should add a tap gesture on your UISlider.
Exemple :
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(sliderTapped:)];
[_slider addGestureRecognizer:tapGestureRecognizer];
In sliderTapped you can get the location and update the value of the slider :
- (void)sliderTapped:(UIGestureRecognizer *)gestureRecognizer {
CGPoint pointTaped = [gestureRecognizer locationInView:gestureRecognizer.view];
CGPoint positionOfSlider = _slider.frame.origin;
float widthOfSlider = _slider.frame.size.width;
float newValue = ((pointTaped.x - positionOfSlider.x) * _slider.maximumValue) / widthOfSlider;
[_slider setValue:newValue];
}
I create an example here : https://github.com/ali59a/tap-and-slide-in-a-UISlider
Here's my modification to the above:
class TapUISlider: UISlider {
func tapAndSlide(gesture: UILongPressGestureRecognizer) {
let pt = gesture.locationInView(self)
let thumbWidth = self.thumbRect().size.width
var value: Float = 0
if (pt.x <= self.thumbRect().size.width / 2) {
value = self.minimumValue
} else if (pt.x >= self.bounds.size.width - thumbWidth / 2) {
value = self.maximumValue
} else {
let percentage = Float((pt.x - thumbWidth / 2) / (self.bounds.size.width - thumbWidth))
let delta = percentage * (self.maximumValue - self.minimumValue)
value = self.minimumValue + delta
}
if (gesture.state == UIGestureRecognizerState.Began) {
UIView.animateWithDuration(0.35, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut,
animations: {
self.setValue(value, animated: true)
super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
}, completion: nil)
} else {
self.setValue(value, animated: false)
super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
}
}
func thumbRect() -> CGRect {
return self.thumbRectForBounds(self.bounds, trackRect: self.bounds, value: self.value)
}
}
Adding swift version of Ali AB.'s answer,
#IBAction func sliderTappedAction(sender: UITapGestureRecognizer)
{
if let slider = sender.view as? UISlider {
if slider.highlighted { return }
let point = sender.locationInView(slider)
let percentage = Float(point.x / CGRectGetWidth(slider.bounds))
let delta = percentage * (slider.maximumValue - slider.minimumValue)
let value = slider.minimumValue + delta
slider.setValue(value, animated: true)
}
}
I didn't check David Williames answer, but I'll post my solution in case someone is looking for another way to do it.
Swift 4
First create a custom UISlider so that it will detect touches on the bar as well :
class CustomSlider: UISlider {
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
return true
}
}
(don't forget to set your slider to be this CustomSlider, on storyboard)
The on viewDidLoad of the view controller that is displaying the slider:
self.slider.addTarget(self, action: #selector(sliderTap), for: .touchDown)
(this is only used to pause the player when moving the slider)
Then, on your UISlider action:
#IBAction func moveSlider(_ sender: CustomSlider, forEvent event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended, .cancelled, .stationary:
//here, start playing if needed
startPlaying()
default:
break
}
}
}
And on your "sliderTap" selector method:
#objc func sliderTap() {
//pause the player, if you want
audioPlayer?.pause()
}
Suggestion: set the player "currentTime" before starting to play:
private func startPlaying() {
audioPlayer?.currentTime = Double(slider.value)
audioPlayer?.play()
}
Updated tsji10dra's answer to Swift 4:
#IBAction func sliderTappedAction(sender: UITapGestureRecognizer) {
if let slider = sender.view as? UISlider {
if slider.isHighlighted { return }
let point = sender.location(in: slider)
let percentage = Float(point.x / slider.bounds.size.width)
let delta = percentage * (slider.maximumValue - slider.minimumValue)
let value = slider.minimumValue + delta
slider.setValue(value, animated: true)
// also remember to call valueChanged if there's any
// custom behaviour going on there and pass the slider
// variable as the parameter, as indicated below
self.sliderValueChanged(slider)
}
}
My solution is quite simple:
class CustomSlider: UISlider {
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let newValue = <calculated_value>
self.setValue(newValue, animated: false)
super.sendActions(for: UIControlEvents.valueChanged)
return true
}}
This works for me in iOS 13.6 & 14.0
No need to add gesture only override beginTracking function like this :
#objc
private func sliderTapped(touch: UITouch) {
let point = touch.location(in: self)
let percentage = Float(point.x / self.bounds.width)
let delta = percentage * (self.maximumValue - self.minimumValue)
let newValue = self.minimumValue + delta
if newValue != self.value {
value = newValue
sendActions(for: .valueChanged)
}
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
sliderTapped(touch: touch)
return true
}
I completed #DWilliames solution for a UISlider subclass containing minimum and maximumValueImages.
Additionally I implemented a functionality for user touches in the areas outside the trackArea (means either the area around the minimum or the maximumValueImage). Touching these areas moves the slider/changes the value in intervals.
- (void) tapAndSlide: (UILongPressGestureRecognizer*) gesture {
CGPoint touchPoint = [gesture locationInView: self];
CGRect trackRect = [self trackRectForBounds: self.bounds];
CGFloat thumbWidth = [self thumbRectForBounds: self.bounds trackRect: trackRect value: self.value].size.width;
CGRect trackArea = CGRectMake(trackRect.origin.x, 0, trackRect.size.width, self.bounds.size.height);
CGFloat value;
if (CGRectContainsPoint(trackArea, touchPoint)) {
if (touchPoint.x <= trackArea.origin.x + thumbWidth/2.0) {
value = self.minimumValue;
}
else if (touchPoint.x >= trackArea.origin.x + trackArea.size.width - thumbWidth/2.0) {
value = self.maximumValue;
}
else {
CGFloat percentage = (touchPoint.x - trackArea.origin.x - thumbWidth/2.0)/(trackArea.size.width - thumbWidth);
CGFloat delta = percentage*(self.maximumValue - self.minimumValue);
value = self.minimumValue + delta;
}
if (value != self.value) {
if (gesture.state == UIGestureRecognizerStateBegan) {
[UIView animateWithDuration: 0.2 delay: 0 options: UIViewAnimationOptionCurveEaseInOut animations: ^{
[self setValue: value animated: YES];
} completion: ^(BOOL finished) {
[self sendActionsForControlEvents: UIControlEventValueChanged];
}];
}
else {
[self setValue: value animated: YES];
[self sendActionsForControlEvents: UIControlEventValueChanged];
}
}
}
else {
if (gesture.state == UIGestureRecognizerStateBegan) {
if (touchPoint.x <= trackArea.origin.x) {
if (self.value == self.minimumValue) return;
value = self.value - 1.5;
}
else {
if (self.value == self.maximumValue) return;
value = self.value + 1.5;
}
CGFloat duration = 0.1;
[UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveEaseInOut animations: ^{
[self setValue: value animated: YES];
} completion: ^(BOOL finished) {
[self sendActionsForControlEvents: UIControlEventValueChanged];
}];
}
}
}
To expand on the answer of Khang Azun- for swift 5 put the following in a UISlider custom class:
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let percent = Float(touch.location(in: self).x / bounds.size.width)
let delta = percent * (maximumValue - minimumValue)
let newValue = minimumValue + delta
self.setValue(newValue, animated: false)
super.sendActions(for: UIControl.Event.valueChanged)
return true
}
At the risk of being chastised by the iOS pure community...
Here is a solution for Xamarin iOS C# converted from David Williames Answer.
Sub class UISlider:
[Register(nameof(UISliderCustom))]
[DesignTimeVisible(true)]
public class UISliderCustom : UISlider
{
public UISliderCustom(IntPtr handle) : base(handle) { }
public UISliderCustom()
{
// Called when created from code.
Initialize();
}
public override void AwakeFromNib()
{
// Called when loaded from xib or storyboard.
Initialize();
}
void Initialize()
{
// Common initialization code here.
var longPress = new UILongPressGestureRecognizer(tapAndSlide);
longPress.MinimumPressDuration = 0;
//longPress.CancelsTouchesInView = false;
this.AddGestureRecognizer(longPress);
this.UserInteractionEnabled = true;
}
private void tapAndSlide(UILongPressGestureRecognizer gesture)
{
System.Diagnostics.Debug.WriteLine($"{nameof(UISliderCustom)} RecognizerState {gesture.State}");
// need to propagate events down the chain
// I imagine iOS does something similar
// for whatever recogniser on the thumb control
// It's not enough to set CancelsTouchesInView because
// if clicking on the track away from the thumb control
// the thumb gesture recogniser won't pick it up anyway
switch (gesture.State)
{
case UIGestureRecognizerState.Cancelled:
this.SendActionForControlEvents(UIControlEvent.TouchCancel);
break;
case UIGestureRecognizerState.Began:
this.SendActionForControlEvents(UIControlEvent.TouchDown);
break;
case UIGestureRecognizerState.Changed:
this.SendActionForControlEvents(UIControlEvent.ValueChanged);
break;
case UIGestureRecognizerState.Ended:
this.SendActionForControlEvents(UIControlEvent.TouchUpInside);
break;
case UIGestureRecognizerState.Failed:
//?
break;
case UIGestureRecognizerState.Possible:
//?
break;
}
var pt = gesture.LocationInView(this);
var thumbWidth = CurrentThumbImage.Size.Width;
var value = 0f;
if (pt.X <= thumbWidth / 2)
{
value = this.MinValue;
}
else if (pt.X >= this.Bounds.Size.Width - thumbWidth / 2)
{
value = this.MaxValue;
}
else
{
var percentage = ((pt.X - thumbWidth / 2) / (this.Bounds.Size.Width - thumbWidth));
var delta = percentage * (this.MaxValue - this.MinValue);
value = this.MinValue + (float)delta;
}
if (gesture.State == UIGestureRecognizerState.Began)
{
UIView.Animate(0.35, 0, UIViewAnimationOptions.CurveEaseInOut,
() =>
{
this.SetValue(value, true);
},
null);
}
else
{
this.SetValue(value, animated: false);
}
}
}
From Apple,
https://developer.apple.com/forums/thread/108317
Now this works fine on iOS 10 and iOS 11. You can slide as usual and thanks to the above code you can tap on slider and it slides automatically. However in iOS 12 this doesn't work. You have to force touch on it for tap to work
Here is my solution that works :
import UIKit
class CustomSlider: UISlider {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
addTapGesture()
}
private func addTapGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
addGestureRecognizer(tap)
}
#objc private func handleTap(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: self)
let percent = minimumValue + Float(location.x / bounds.width) * maximumValue
setValue(percent, animated: true)
sendActions(for: .valueChanged)
}
}

Pan, zoom, rotate UIView around anchorPoint using UIGestureRecognizer (iOS)

I'm trying to rotate, pan and zoom an UIView using UIGestureRecognizers. The recognisers are added to the superview, while the rotation,zoom etc is applied to a subview (_baseImage) this is to enable me to overlay other things on top of the subview in future while still receiving the gesture events.
The idea is that the UIView should scale/rotate around an "anchor" point on the subview underneath the two touch points as this seems the most natural. What I'm having problems with is the position of the subview after setting the anchorPoint, and also that the scale and rotation doesn't seem to use the set anchorPoint. My lack of understanding of overlapping co-ordinate systems/CGAffine transforms may be getting me into trouble. Code is pulled from various examples.
Here's my code as it stands at the moment:
-(void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view
{
CGPoint oldOrigin = view.frame.origin;
view.layer.anchorPoint = anchorPoint;
CGPoint newOrigin = view.frame.origin;
CGPoint transition;
transition.x = newOrigin.x - oldOrigin.x;
transition.y = newOrigin.y - oldOrigin.y;
view.center = CGPointMake (view.center.x - transition.x, view.center.y - transition.y);
}
- (void) updateTransformWithOffset: (CGPoint) translation
{
// Create a blended transform representing translation,
// rotation, and scaling
_baseImage.transform = CGAffineTransformMakeTranslation(translation.x + tx, translation.y + ty);
_baseImage.transform = CGAffineTransformRotate(_baseImage.transform, theta);
_baseImage.transform = CGAffineTransformScale(_baseImage.transform, scale, scale);
}
- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)uigr {
if (uigr.state == UIGestureRecognizerStateBegan) {
UIView *piece = self.view;
CGPoint locationInView = [uigr locationInView:_baseImage];
myFrame = _baseImage.frame;
CGPoint newAnchor = CGPointMake( (locationInView.x / piece.bounds.size.width), (locationInView.y / piece.bounds.size.height ));
[self setAnchorPoint:newAnchor forView:_baseImage];
}
}
- (void) handlePinch: (UIPinchGestureRecognizer *) uigr
{
[self adjustAnchorPointForGestureRecognizer:uigr];
if (uigr.state == UIGestureRecognizerStateBegan) {
initScale = scale;
}
scale = initScale*uigr.scale;
[self updateTransformWithOffset:CGPointZero];
}
I've found a solution to my specific problem with the anchorPoint. I was trying to apply transforms to a UIView added in Interface Builder which caused problems with the anchor point and setting new centre point, removing then re-adding the subview seemed to fix it. If anyone has similar problems here is the final code I used on my view controller, it does scale,rotate and pan on a UIView using the touch position as centre for rotate and scale:
#import "ViewController.h"
#interface ViewController (){
CGFloat tx; // x translation
CGFloat ty; // y translation
CGFloat scale; // zoom scale
CGFloat theta; // rotation angle
CGFloat initScale ;
CGFloat initTheta ;
}
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIRotationGestureRecognizer *rotationGesture = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:#selector(handleRotation:)];
[rotationGesture setDelegate:self];
[self.view addGestureRecognizer:rotationGesture];
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinch:)];
[pinchGesture setDelegate:self];
[self.view addGestureRecognizer:pinchGesture];
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[panGesture setDelegate:self];
[panGesture setMinimumNumberOfTouches:1];
[panGesture setMaximumNumberOfTouches:1];
[self.view addGestureRecognizer:panGesture];
_baseImage.transform = CGAffineTransformIdentity;
tx = 0.0f; ty = 0.0f; scale = 1.0f; theta = 0.0f;
scale = 1.0;
//removing and adding back to the view seems to fix problems with anchor point I was having, I suspect because of IB layout/scaling and constraints etc
UIView *mySuperView =_baseImage.superview;
[_baseImage removeFromSuperview];
[mySuperView addSubview:_baseImage];
}
-(void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)myview
{
CGPoint oldOrigin = myview.frame.origin;
myview.layer.anchorPoint = anchorPoint;
CGPoint newOrigin = myview.frame.origin;
CGPoint transition;
transition.x = (newOrigin.x - oldOrigin.x);
transition.y = (newOrigin.y - oldOrigin.y);
CGPoint myNewCenter = CGPointMake (myview.center.x - transition.x, myview.center.y - transition.y);
myview.center = myNewCenter;
}
- (void) updateTransformWithOffset: (CGPoint) translation
{
// Create a blended transform representing translation,
// rotation, and scaling
_baseImage.transform = CGAffineTransformMakeTranslation(translation.x + tx, translation.y + ty);
_baseImage.transform = CGAffineTransformRotate(_baseImage.transform, theta);
_baseImage.transform = CGAffineTransformScale(_baseImage.transform, scale, scale);
}
- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)uigr {
if (uigr.state == UIGestureRecognizerStateBegan) {
tx =_baseImage.transform.tx;
ty =_baseImage.transform.ty;
CGPoint locationInView = [uigr locationInView:_baseImage];
CGPoint newAnchor = CGPointMake( (locationInView.x / _baseImage.bounds.size.width), (locationInView.y / _baseImage.bounds.size.height ));
[self setAnchorPoint:newAnchor forView:_baseImage];
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
// if the gesture recognizers are on different views, don't allow simultaneous recognition
if (gestureRecognizer.view != otherGestureRecognizer.view)
return NO;
if (![gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && ![otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
return YES;
}
return NO;
}
- (void) handleRotation: (UIRotationGestureRecognizer *) uigr
{
if (uigr.state == UIGestureRecognizerStateBegan) {
initTheta = theta;
}
theta = initTheta+uigr.rotation;
[self adjustAnchorPointForGestureRecognizer:uigr];
[self updateTransformWithOffset:CGPointZero];
}
- (void) handlePinch: (UIPinchGestureRecognizer *) uigr
{
if (uigr.state == UIGestureRecognizerStateBegan) {
initScale = scale;
}
scale = initScale*uigr.scale;
[self adjustAnchorPointForGestureRecognizer:uigr];
[self updateTransformWithOffset:CGPointZero];
}
- (void) handlePan: (UIPanGestureRecognizer *) uigr
{
CGPoint translation = [uigr translationInView:_baseImage.superview];
[self adjustAnchorPointForGestureRecognizer:uigr];
[self updateTransformWithOffset:translation];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
The OP's original solution translated to Swift 2.0. I left out the workaround since it doesn't seem to be an issue anymore.
class ViewController: UIViewController {
var tx:CGFloat = 0.0 // x translation
var ty:CGFloat = 0.0 // y translation
var scale:CGFloat = 1.0 // zoom scale
var theta:CGFloat = 0.0 // rotation angle
var initScale:CGFloat = 1.0
var initTheta:CGFloat = 0.0
var transformedView: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
transformedView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
transformedView.backgroundColor = UIColor.blueColor()
view.addSubview(transformedView)
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(OHPlastyViewController.handleRotation(_:)))
rotationGesture.delegate = self
view.addGestureRecognizer(rotationGesture)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(OHPlastyViewController.handlePinch(_:)))
pinchGesture.delegate = self
view.addGestureRecognizer(pinchGesture)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(OHPlastyViewController.handlePan(_:)))
panGesture.delegate = self
panGesture.minimumNumberOfTouches = 1
panGesture.maximumNumberOfTouches = 1
view.addGestureRecognizer(panGesture)
}
func setAnchorPoint(anchorPoint: CGPoint, forView myView: UIView) {
let oldOrigin: CGPoint = myView.frame.origin
myView.layer.anchorPoint = anchorPoint
let newOrigin = myView.frame.origin
let transition = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)
let myNewCenter = CGPoint(x: myView.center.x - transition.x, y: myView.center.y - transition.y)
myView.center = myNewCenter
}
func updateTransformWithOffset(translation: CGPoint) {
transformedView.transform = CGAffineTransformMakeTranslation(translation.x + tx, translation.y + ty)
transformedView.transform = CGAffineTransformRotate(transformedView.transform, theta)
transformedView.transform = CGAffineTransformScale(transformedView.transform, scale, scale)
}
func adjustAnchorPointForGestureRecognizer(recognizer: UIGestureRecognizer) {
if (recognizer.state == .Began) {
tx = transformedView.transform.tx
ty = transformedView.transform.ty
let locationInView = recognizer.locationInView(transformedView)
let newAnchor = CGPoint(x: (locationInView.x / transformedView.bounds.size.width), y: (locationInView.y / transformedView.bounds.size.height))
setAnchorPoint(newAnchor, forView: transformedView)
}
}
func handleRotation(recognizer: UIRotationGestureRecognizer) {
if recognizer.state == .began {
initTheta = theta
}
theta = initTheta + recognizer.rotation
adjustAnchorPointForGestureRecognizer(recognizer)
updateTransformWithOffset(CGPointZero)
}
func handlePinch(recognizer: UIPinchGestureRecognizer) {
if recognizer.state == .began {
initScale = scale
}
scale = initScale * recognizer.scale
adjustAnchorPointForGestureRecognizer(recognizer)
updateTransformWithOffset(CGPointZero)
}
func handlePan(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(transformedView.superview)
adjustAnchorPointForGestureRecognizer(recognizer)
updateTransformWithOffset(translation)
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
if !gestureRecognizer.isKindOfClass(UITapGestureRecognizer.self) && !otherGestureRecognizer.isKindOfClass(UITapGestureRecognizer.self) {
return true
}
return false
}
}

Resources