Grow a UIImageView while UILongPressGestureRecognizer active - ios

I want to add an UIImageView to my view at the users touch location and have the UIImageView grow while the user is holding their finger down. Think of a ballon being blown up. I want the center of the UIImageView to remain at the user's touch location while its growing.
I figured the best way would be a UILongPressGestureRecognizer and wrote the below. This does work as I planed except the visual effect is somewhat choppy and clumsy.
Is there any way that I can animate the UIImageView's size until the UILongPressGestureRecognizer calls UIGestureRecognizerStateEnded?
Or, is there a better way to do this altogether?
declared in .h: CGPoint longPressLocation;
.m:
- (IBAction) handleInflation:(UILongPressGestureRecognizer *) inflateGesture {
longPressLocation= [inflateGesture locationInView:self.view];
switch (inflateGesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(#"Long press Began .................");
inflateTimer = [NSTimer scheduledTimerWithTimeInterval:.4 target:self selector:#selector(inflate) userInfo:nil repeats:YES];
UIImage *tempImage=[UIImage imageNamed:#"bomb.png"];
UIImageView *inflatableImageView = [[UIImageView alloc] initWithFrame:CGRectMake(longPressLocation.x-tempImage.size.width/2,
longPressLocation.y-tempImage.size.height/2,
tempImage.size.width, tempImage.size.height)];
inflatableImageView.image = tempImage;
[bonusGame addSubview:inflatableImageView];
inflatable=inflatableImageView;
}
break;
case UIGestureRecognizerStateChanged:{
NSLog(#"Long press Changed .................");
}
break;
case UIGestureRecognizerStateEnded:{
NSLog(#"Long press Ended .................");
[inflateTimer invalidate];
}
break;
default:
break;
}
}
-(void)inflate{
inflatable.frame=CGRectMake(inflatable.frame.origin.x,inflatable.frame.origin.y , inflatable.bounds.size.width+15, inflatable.bounds.size.height+15);
inflatable.center=longPressLocation;
}
Final Working Code:
- (IBAction) handleInflation:(UILongPressGestureRecognizer *) inflateGesture {
inflateGesture.minimumPressDuration = .01;
longPressLocation= [inflateGesture locationInView:self.view];
switch (inflateGesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(#"Long press Began .................");
inflateStart = [NSDate date];
inflateDisplayLink = [NSClassFromString(#"CADisplayLink") displayLinkWithTarget:self selector:#selector(inflate)];
[inflateDisplayLink setFrameInterval:1];
[inflateDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
UIImage *tempImage=[UIImage imageNamed:#"bomb.png"];
UIImageView *inflatableImageView = [[UIImageView alloc] initWithFrame:CGRectMake(longPressLocation.x-tempImage.size.width/2,
longPressLocation.y-tempImage.size.height/2,
tempImage.size.width, tempImage.size.height)];
inflatableImageView.image = tempImage;
[bonusGame addSubview:inflatableImageView];
inflatable=inflatableImageView;
}
break;
case UIGestureRecognizerStateChanged:{
NSLog(#"Long press Changed .................");
}
break;
case UIGestureRecognizerStateEnded:{
NSLog(#"Long press Ended .................");
[inflateDisplayLink invalidate];
}
break;
default:
break;
}
}
-(void)inflate{
NSDate *inflateEnd = [NSDate date];
NSTimeInterval inflateInterval;
inflateInterval = ([inflateEnd timeIntervalSince1970] - [inflateStart timeIntervalSince1970])*25;
inflatable.frame=CGRectMake(inflatable.frame.origin.x,
inflatable.frame.origin.y ,
inflatable.bounds.size.width+inflateInterval,
inflatable.bounds.size.height+inflateInterval);
inflatable.center=longPressLocation;
if(inflatable.bounds.size.width>200){
[inflateDisplayLink invalidate];
}
}

A timer may not be smooth. Instead check out CADisplayLink, which will provide a delegate callback whenever the device's screen is redrawn (~60hz), so you will get a per frame chance to adjust your balloon size.
Another thing to consider is the time between refreshes isn't constant, it could be a lot slower than when the last refresh occured, so if you are incrementing the size by a constant 15 every time you get a callback, then the animation may not seem smooth.
To combat this, when you start the animation take a timestamp and hold onto it, then when you inflate the balloon take another timestamp and determine the difference between now and the last redraw, then multiply the difference by some value, which will ensure a constant smooth size growth - this is called a timestep.
https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/Reference/Reference.html

Getting creative here, but I think this might work:
You start an animation that animates the images frame from its current size towards a maximum size.
Start the animation as soon as you detect the long press gesture.
If the gesture ends, get the current frame size from the presentationLayer of the animated view/image and update the view/image's frame to that size by starting a new animation with UIViewAnimationOptionBeginFromCurrentState so that the old animation will stop.
That should give you a smoothly growing balloon.

Try this....
imageView = [[UIImageView alloc] initWithFrame:(CGRectMake(120, 100, 30, 30))];
imageView.backgroundColor = [UIColor redColor];
[self.view addSubview:imageView];
imageView.userInteractionEnabled = TRUE;
UILongPressGestureRecognizer *g = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[g setMinimumPressDuration:0.001];
[imageView addGestureRecognizer:g];
And Add these methods
-(void)longPress:(UITapGestureRecognizer *)t
{
if (t.state == UIGestureRecognizerStateBegan)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeIncrc) userInfo:nil repeats:TRUE];
}
else if (t.state == UIGestureRecognizerStateEnded || t.state == UIGestureRecognizerStateCancelled)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(makeDec) userInfo:nil repeats:TRUE];
}
}
-(void)makeIncrc
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:(UIViewAnimationCurveLinear)];
CGRect frame = imageView.frame;
frame.origin.x = frame.origin.x - 5;
frame.origin.y = frame.origin.y - 5;
frame.size.width = frame.size.width + 10;
frame.size.height = frame.size.height + 10;
imageView.frame = frame;
[UIView commitAnimations];
}
-(void)makeDec
{
if (imageView.frame.size.width < 20 || imageView.frame.size.height < 20)
{
[timer invalidate];
}
else
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationCurve:(UIViewAnimationCurveLinear)];
CGRect frame = imageView.frame;
frame.origin.x = frame.origin.x + 5;
frame.origin.y = frame.origin.y + 5;
frame.size.width = frame.size.width - 10;
frame.size.height = frame.size.height - 10;
imageView.frame = frame;
[UIView commitAnimations];
}
}

Here's a Swift version. I had to replace invalidate() calls with "paused" in order to avoid runaway inflation.
var inflateDisplayLink: CADisplayLink?
var inflateStart: NSDate!
var longPressLocation: CGPoint!
var inflatable: UIImageView!
func handleInflation(inflateGesture: UILongPressGestureRecognizer) {
longPressLocation = inflateGesture.locationInView(self.view)
let imageView = inflateGesture.view as! UIImageView
switch (inflateGesture.state) {
case .Began:
println("Long press Began .................")
inflateStart = NSDate()
let tempImageView = UIImageView(image: imageView.image)
tempImageView.contentMode = .ScaleAspectFit
tempImageView.frame = imageView.frame
self.view.addSubview(tempImageView)
inflatable = tempImageView
if inflateDisplayLink == nil {
inflateDisplayLink = CADisplayLink(target: self, selector: Selector("inflate"))
inflateDisplayLink!.frameInterval = 1
inflateDisplayLink!.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
} else {
inflateDisplayLink!.paused = false
}
break
case .Changed:
println("Long press Changed .................")
break
case .Ended:
println("Long press Ended .................")
inflateDisplayLink!.paused = true
break
default:
break
}
}
func inflate() {
var inflateEnd = NSDate()
// Convert from Double to CGFloat, due to bug (?) on iPhone5
var inflateInterval: CGFloat = CGFloat((Double(inflateEnd.timeIntervalSince1970) - Double(inflateStart.timeIntervalSince1970))) * 5.0
inflatable.frame = CGRectMake(inflatable.frame.origin.x, inflatable.frame.origin.y, inflatable.bounds.size.width + inflateInterval, inflatable.bounds.size.height + inflateInterval)
inflatable.center = longPressLocation.center
if inflatable.bounds.size.width > 200 {
inflateDisplayLink!.paused = true
}
}

Related

Why UILongPressGestureRecognizer behaves differently on devices released after iPhone 6?

In my code, I have an area on which when users taps and holds, UILongPressGestureRecognizer causes an animation to run and at the end of animation a method gets called.
(It's basically a long tap on a small area square which contains one CGPoint so that it starts a long press animation and when it finishes, the point is removed.)
The problem: On iPhone 5s,SE and 6, UILongPressGestureRecognizer works fine but on any device released after that, it gets dismissed very quickly and I have tried it on at least 11 different devices.
Why is that?!
I appreciate any help.
-(void)prepareLongPressGesture{
_longGesture = [[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:#selector(handleLongGesture:)];
_longGesture.delegate = self;
_longGesture.allowableMovement = self.frame.size.width*0.15;
_longGesture.minimumPressDuration = 1.0;
[self addGestureRecognizer:_longGesture];
}
- (void)handleLongGesture:(UILongPressGestureRecognizer*)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
[self removeModifiablePoint];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
if (self.animatedView.superview) {
[self.animatedView removeFromSuperview];
}
} break;
default:
break;
}
}
- (void)removeModifiablePoint {
CGFloat sizeOFLongPressAnimation = 0.0;
if (IS_IPAD) {
sizeOFLongPressAnimation = 0.15*self.frame.size.width;
}else{
sizeOFLongPressAnimation = 0.27*self.frame.size.width;
}
if (!_animatedView) {
self.animatedView =
[[UIView alloc] initWithFrame:CGRectMake(0, 0, sizeOFLongPressAnimation, sizeOFLongPressAnimation)];
self.animatedView.alpha = 0.7f;
self.animatedView.layer.borderColor = [UIColor redColor].CGColor;
self.animatedView.layer.borderWidth = 3.f;
self.animatedView.layer.cornerRadius = sizeOFLongPressAnimation / 2.0f;
self.progressView = [[UIView alloc] initWithFrame:self.animatedView.bounds];
self.progressView.backgroundColor = [UIColor redColor];
self.progressView.layer.cornerRadius = sizeOFLongPressAnimation / 2.0f;
[self.animatedView addSubview:self.progressView];
}
CGPoint center = [self.modifiablePointsArray[_modifiablePointIndex] CGPointValue];
self.animatedView.center = center;
[self addSubview:self.animatedView];
self.progressView.transform =
CGAffineTransformScale(CGAffineTransformIdentity, 0.1, 0.1f);
[UIView animateWithDuration:2.f animations:^{
self.progressView.transform = CGAffineTransformIdentity;
}completion:^(BOOL finished) {
if (finished) {
[self.animatedView removeFromSuperview];
[self.modifiablePointsArray removeObjectAtIndex:_modifiablePointIndex];
[self setNeedsDisplay];
}
}];
}

how to make a sliding up panel like the Google Maps app?

I'm looking for something like AndroidSlidingUpPanel for iOS. I found MBPullDownController, but it requires two ViewControllers to use, and requires a big change to the architecture of the app I'm working on to implement.
I just want something that adds a subview in an existing view controller. How would I go about doing that?
I use sliding up panels in my iOS apps quite a lot and I've found that the trick is to add a custom view to a view controller in the storyboard (or xib file) but to set its frame so that it is off the screen. You can ensure that the view stays off screen on any device using layout constraints.
Then it's just a case of animating the view on screen when appropriate. e.g.:
- (IBAction)showPanel:(id)sender
{
// panelShown is an iVar to track the panel state...
if (!panelShown) {
// myConstraint is an IBOutlet to the appropriate constraint...
// Use this method for iOS 8+ otherwise use a frame based animation...
myConstraint.constant -= customView.frame.size.height;
[UIView animateWithDuration:0.5 animations:^{
[self.view setNeedsLayout];
}];
}
else {
myConstraint.constant += customView.frame.size.height;
[UIView animateWithDuration:0.5 animations:^{
[self.view setNeedsLayout];
}];
}
}
If you want to have just a swipe up/down and that will reveal the panel you can use UISwipeGestureRecognizer like so:
- (void)viewDidLoad
{
[super viewDidLoad];
// iVar
swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(didSwipe:)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
[self.view addGestureRecognizer:swipeUp];
// Do the same again with swipeDown using UISwipeGestureRecognizerDirectionDown...
}
- (void)didSwipe:(UIGestureRecognizer *)swipe
{
if (swipe == swipeUp) {
// Show panel (see above)...
} else {
// Hide panel (see above)...
}
}
If you want the panel to track your finger like when you open the Control Center, then you can use UIPanGestureRecognizer and get the translationInView: and velocityInView: and adjust the panel accordingly. Here is a snippet of code that tracks finger movement but using the touchesBegan:withEvent: - (void)touchesMoved:withEvent: and - (void)touchesEnded:withEvent: methods in a UIViewController to give you a taste:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Don't worry too much about buttonView this is another view that I animate upwards to get out the way of the panel as it slides in from the left...
[super touchesBegan:touches withEvent:event];
CGPoint loc = [[touches anyObject] locationInView:self.view];
// Save last touch for reference...
lastTouch = loc;
// leftBeginRect is an area where the user can start to drag the panel...
// trackFinger defines whether the panel should move with the users gestures or not...
if (CGRectContainsPoint(leftBeginRect, loc) && canTrack) {
trackFinger = YES;
}
// Left view is a reference to the panel...
else if (leftView.frame.size.width >= 300) {
// This means that the panel is shown and therefore should track the user's finger back towards the edge of the screen...
CGRect frame = CGRectMake(250, 0, 100, self.view.frame.size.height);
if (CGRectContainsPoint(frame, loc) && canTrack) {
trackFinger = YES;
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
CGPoint loc = [[touches anyObject] locationInView:self.view];
// Need to work out the direction in which the user is panning...
if (lastTouch.x > loc.x) {
currentFingerDirection = RHFingerDirectionLeft;
}
else {
currentFingerDirection = RHFingerDirectionRight;
}
lastTouch = loc;
if (trackFinger) {
if (loc.x <= 300) {
// This means that the panel is somewhere between fully exposed and closed...
// This is where the frame for the left view (and the constraints) are adjusted according to the user's current finger position...
CGRect frame = leftView.frame;
frame.size.width = loc.x;
[leftView setFrame:frame];
leftViewConstraint.constant = loc.x;
if (loc.x <= 80) {
float percentage = loc.x / 80;
int amount = 100 * percentage;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = -amount;
[buttonView setFrame:otherFrame];
constraint.constant = constraintConstant + amount;
}
}
else {
CGRect frame = leftView.frame;
frame.size.width = 300;
[leftView setFrame:frame];
leftViewConstraint.constant = 300;
frame = buttonView.frame;
frame.origin.y = -100;
[buttonView setFrame:frame];
constraint.constant = constraintConstant + 100;
trackFinger = NO;
}
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// This method works out if the panel should pop open or spring closed when the user ends the gesture...
[super touchesEnded:touches withEvent:event];
if (trackFinger) {
CGPoint loc = [[touches anyObject] locationInView:self.view];
if (loc.x >= 50 && currentFingerDirection == RHFingerDirectionRight) {
CGRect frame = leftView.frame;
frame.size.width = 300;
leftViewConstraint.constant = 300;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = -100;
constraint.constant = constraintConstant + 100;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
else if (loc.x <= 250 && currentFingerDirection == RHFingerDirectionLeft) {
CGRect frame = leftView.frame;
frame.size.width = 0;
leftViewConstraint.constant = 0;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = 0;
constraint.constant = constraintConstant;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
else if (loc.x <= 150) {
CGRect frame = leftView.frame;
frame.size.width = 0;
leftViewConstraint.constant = 0;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = 0;
constraint.constant = constraintConstant;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
else {
CGRect frame = leftView.frame;
frame.size.width = 300;
leftViewConstraint.constant = 300;
CGRect otherFrame = buttonView.frame;
otherFrame.origin.y = -100;
constraint.constant = constraintConstant + 100;
[UIView animateWithDuration:0.2 animations:^{
[leftView setFrame:frame];
[buttonView setFrame:otherFrame];
}];
}
trackFinger = NO;
}
currentFingerDirection = RHFingerDirectionNone;
}
The code is quite involved but it results in a nice panel animation that follows your finger like the Control Center.
Sorry for a late response. I created a small lib based on Rob-s answer.
https://github.com/hoomazoid/CTSlidingUpPanel
It's quite simple to use:
#IBOutlet weak var bottomView: UIView!
var bottomController:CTBottomSlideController?;
override func viewDidLoad() {
super.viewDidLoad()
//You can provide nil to tabController and navigationController
bottomController = CTBottomSlideController(parent: view, bottomView: bottomView,
tabController: self.tabBarController!,
navController: self.navigationController, visibleHeight: 64)
//0 is bottom and 1 is top. 0.5 would be center
bottomController?.setAnchorPoint(anchor: 0.7)
}

detect when UIImageView finished scrolling

I have a tall UIImageView scrolling across the screen when a button is pressed. I used a timer to change the y position of the image at a specific speed. The image is 10,000px tall.
Here's my simple code:
-(void) moveImage {
myImage.center = CGPointMake(myImage.center.x, myImage.center.y +6);
}
-(IBAction)StartGame:(id)sender{
moveImageTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(moveImage)userInfo:nil repeats:YES];
[StartGame setHidden:TRUE];
}
Here's what i've tried to stop the timer when the image has scrolled completely through the screen:
-(void) moveImage {
myImage.center = CGPointMake(myImage.center.x, myImage.center.y +6);
if ([myImage == GPointMake(myImage.center.x, myImage.center.y -568) {
[moveImageTimer invalidate];
}
That doesn't work but i thought it would. Can someone tell me why the IF statement doesn't work? Any help is appreciated, thanks
Since you want to stop when the image moves to the bottom corner.
-(void) moveImage {
CGPoint newPoint= CGPointMake(myImage.center.x, myImage.center.y +6);
myImage.center = newPoint;
if ((newPoint.y - (myImage.frame.size.width/2)) >= [UIScreen mainScreen].bounds.size.height)
{
[moveImageTimer invalidate];
}
}
you can check the condition
like if (imageView.center.y > imageView.frame.size.height - self.view.frame.size.height)
here imageView.frame.size.height - self.view.frame.size.height because the image size is twice screen size. You have to set you logic
imageView.center = CGPointMake(imageView.center.x, imageView.center.y + 6);
if (imageView.center.y > imageView.frame.size.height - self.view.frame.size.height)
{
[moveImageTimer invalidate];
}
}

Second animation

I have several buttons located at different sites in the view (storyboard), and I want that when I click on each of them to go to a given point,
To do this, I keep their location and initial size by:
CGPoint originalCenter1;
CGRect originalsize1;
CGPoint originalCenter2;
CGRect originalsize2;
in viewDidLoad
originalCenter1 = self.boton1.center;
originalsize1 = self.boton1.frame;
originalCenter2 = self.boton2.center;
originalsize2 = self.boton2.frame;
and the IBAction associated with each button, the animation ...
-(IBAction)move:(id)sender{
UIButton * pressedbutton = (UIButton*)sender;
[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
CGAffineTransform scale = CGAffineTransformMakeScale(3.0, 3.0);
pressedbutton.transform = scale;
switch(pressedbutton.tag)
{
case 0:
pressedbutton.center = CGPointMake(784, 340);
break;
case 1:
pressedbutton.center = CGPointMake(784, 340);
break;
When already all have moved, I have a button Refresh that puts me to the initial position.
-(IBAction)refresh:(id)sender{
self.boton1.frame = originalsize1;
self.boton1.center = originalCenter1;
self.boton1.alpha=1;
self.boton2.frame = originalsize2;
self.boton2.center = originalCenter2;
self.boton2.alpha=1;
The problem is that the next time that pulse buttons, move to the position shown in the animation but the "scale" effect, doesn't work !!
Any help ??
Thanks.
I think you should use block try this...
- (IBAction)tap:(UITapGestureRecognizer *)gesture
{
CGPoint tapLocation = [gesture locationInView:self.view1];
for (UIView *view in self.view1.subviews) {
if (CGRectContainsPoint(view.frame, tapLocation)) {
[UIView animateWithDuration:5.0 animations:^{
[self setRandomLocationForView:view];
}];
}
}
}
- (void)setRandomLocationForView:(UIView *)view
{
[view sizeToFit];
CGRect sinkBounds = CGRectInset(self.view1.bounds, view.frame.size.width/2, view.frame.size.height/2);
CGFloat x = your location.x;
CGFloat y = yout location.y;
view.center = CGPointMake(x, y);
}

Creating a continuous animation

I want to create an animation that moves up or down the screen according to how fast the user taps the screen. The problem I am having is that I don't know how to create an infinite loop so I am firing a timer which presents issues. Here is my current code.
-(void)setPosOfCider {
CGFloat originalY = CGRectGetMinY(cider.frame);
float oY = originalY;
float posY = averageTapsPerSecond * 100;
float dur = 0;
dur = (oY - posY) / 100;
[UIImageView animateWithDuration:dur animations:^(void) {
cider.frame = CGRectMake(768, 1024 - posY, 768, 1024);
}];
}
Suggested fix(Doesn't work):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
scroll.pagingEnabled = YES;
scroll.scrollEnabled = YES;
scroll.contentSize = CGSizeMake(768 * 3, 1024); // 3 pages wide.
scroll.delegate = self;
self.speedInPointsPerSecond = 200000;
self.tapEvents = [NSMutableArray array];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self startDisplayLink];
}
-(IBAction)tapped {
[self.tapEvents addObject:[NSDate date]];
// if less than two taps, no average speed
if ([self.tapEvents count] < 1)
return;
// only average the last three taps
if ([self.tapEvents count] > 2)
[self.tapEvents removeObjectAtIndex:0];
// now calculate the average taps per second of the last three taps
NSDate *start = self.tapEvents[0];
NSDate *end = [self.tapEvents lastObject];
self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
CGFloat y = 1024 - (self.averageTapsPerSecond * 100.0);
return y;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval now = CACurrentMediaTime();
CGFloat elapsed = now - self.lastTime;
self.lastTime = now;
if (elapsed <= 0) return;
CGPoint center = self.cider.center;
CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];
if (center.y == destinationY)
{
// we don't need to move it at all
return;
}
else if (center.y > destinationY)
{
// we need to move it up
center.y -= self.speedInPointsPerSecond * elapsed;
if (center.y < destinationY)
center.y = destinationY;
}
else
{
// we need to move it down
center.y += self.speedInPointsPerSecond * elapsed;
if (center.y > destinationY)
center.y = destinationY;
}
self.cider.center = center;
}
An even simpler approach is to employ the UIViewAnimationOptionBeginFromCurrentState option when using block-based animations:
[UIView animateWithDuration:2.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
animations:^{
// set the new frame
}
completion:NULL];
That stops the current block-based animation and starts the new one from the current location. Note, effective iOS 8 and later, this is now the default behavior, generally obviating the need for the UIViewAnimationOptionBeginFromCurrentState altogether. In fact, the default behavior considers not only the position of the view in question, but also the current velocity, ensuring a smooth transition. See WWDC 2014 video Building Interruptible and Responsive Interactions for more information.)
Another approach would be to have your taps (which adjust the averageTapsPerSecond) to stop any existing animation, grab the view's current frame from the presentation layer (which reflects the state of the view mid-animation), and then just start a new animation:
// grab the frame from the presentation layer (which is the frame mid-animation)
CALayer *presentationLayer = self.viewToAnimate.layer.presentationLayer;
CGRect frame = presentationLayer.frame;
// stop the animation
[self.viewToAnimate.layer removeAllAnimations];
// set the frame to be location we got from the presentation layer
self.viewToAnimate.frame = frame;
// now, starting from that location, animate to the new frame
[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
self.viewToAnimate.frame = ...; // set the frame according to the averageTapsPerSecond
} completion:nil];
In iOS 7, you would use the rendition of animateWithDuration with the initialSpringVelocity: parameter, to ensure a smooth transition from the object's current movement to the new animation. See the aforementioned WWDC video for examples.
Original answer:
To adjust a view on the basis of the number of taps per second, you could use a CADisplayLink, which is like a NSTimer, but ideally suited for animation efforts like this. Bottom line, translate your averageTapsPerSecond into a y coordinate, and then use the CADisplayLink to animate the moving of some view to that y coordinate:
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval lastTime;
#property (nonatomic) CGFloat speedInPointsPerSecond;
#property (nonatomic, strong) NSMutableArray *tapEvents;
#property (nonatomic) CGFloat averageTapsPerSecond;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.speedInPointsPerSecond = 200.0;
self.tapEvents = [NSMutableArray array];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
[self.tapEvents addObject:[NSDate date]];
// if less than two taps, no average speed
if ([self.tapEvents count] < 2)
return;
// only average the last three taps
if ([self.tapEvents count] > 3)
[self.tapEvents removeObjectAtIndex:0];
// now calculate the average taps per second of the last three taps
NSDate *start = self.tapEvents[0];
NSDate *end = [self.tapEvents lastObject];
self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}
- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
CGFloat y = 480 - self.averageTapsPerSecond * 100.0;
return y;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self startDisplayLink];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self stopDisplayLink];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval now = CACurrentMediaTime();
CGFloat elapsed = now - self.lastTime;
self.lastTime = now;
if (elapsed <= 0) return;
CGPoint center = self.viewToAnimate.center;
CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];
if (center.y == destinationY)
{
// we don't need to move it at all
return;
}
else if (center.y > destinationY)
{
// we need to move it up
center.y -= self.speedInPointsPerSecond * elapsed;
if (center.y < destinationY)
center.y = destinationY;
}
else
{
// we need to move it down
center.y += self.speedInPointsPerSecond * elapsed;
if (center.y > destinationY)
center.y = destinationY;
}
self.viewToAnimate.center = center;
}
#end
Hopefully this illustrates the idea.
Also, to use the above, make sure you've added the QuartzCore framework to your project (though in Xcode 5, it seems to do that for you).
For standard, constant speed animation, you can use the following:
You would generally do this with an animation that repeats (i.e. UIViewAnimationOptionRepeat) and that autoreverses (i.e. UIViewAnimationOptionAutoreverse):
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
cider.frame = ... // specify the end destination here
}
completion:nil];
For a simple up-down-up animation, that's all you need. No timers needed.
There is one line you need to add to Rob's answer.
[UIView animateWithDuration:dur delay:del options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
[UIView setAnimationRepeatCount:HUGE_VAL];
//Your code here
} completion:nil];
If you need to stop that animation, use this line (remember to add QuartzCore framework):
[view.layer removeAllAnimations];

Resources