I have one UIImageView and number of UIImageView which are entering in screen after some time interval. I want to check if that one ImageView is collided with any others.
Please help me.
The general process for detecting collisions between rectangular shaped views is to use CGRectIntersectsRect() to see if the frames of two views intersect. So, if you have a NSMutableArray of UIImageView objects, you can perform a fast enumeration through them and look for the collision, something like:
for (UIView* view in self.imageViewsArray)
{
if (CGRectIntersectsRect(view.frame, viewToDetectCollisionWith.frame))
{
// do whatever you want when you detect the collision
}
}
Or, you can use the enumerateObjectsUsingBlock which uses fast enumeration, but gives you both the numeric index, idx, and the individual UIView objects in the array in a single statement:
[self.imageViewsArray enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
if (CGRectIntersectsRect(view.frame, viewToDetectCollisionWith.frame))
{
// do whatever you want when you detect the collision
}
}];
Original answer:
If you're animating the UIImageView objects via the various automated animation techniques, you have to use something like CADisplayLink to check for collisions because iOS is taking care of the animation and you otherwise are not informed of the frame of the various views in the middle of an animation. The CADisplayLink informs your app every time the animation has progressed, so you get information about the location of views as the animation progresses. Sounds like you're not availing yourself of built in animation techniques, but rather using a NSTimer to manually adjust frames, so you might not need the below code. But if you ever pursue a more automated animation, you can use the following technique.
What you can do is use a CADisplayLink to get information about the screen while the animation is in progress:
#import <QuartzCore/QuartzCore.h>
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkHandler)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
You might even want to store that in a class property so you can add it and remove it as the view appears and disappears:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkHandler)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
self.displayLink = nil;
}
Then, you can start your animation. I'm just using the standard block-based animation to continually animate the changing of two image view frames, but you'll obviously do whatever is appropriate for your app:
[UIView animateWithDuration:4.0
delay:0.5
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
self.imageView1.frame = ... // I set the frame here
self.imageView2.frame = ... // I set the frame here
}
completion:nil];
Now I can detect when these two frames collide (i.e., whether their frames intersect) with this CADisplayLink handler which grabs the relevant presentationLayer properties to get the "in progress" frame coordinates:
- (void)displayLinkHandler
{
id presentationLayer1 = self.imageView1.layer.presentationLayer;
id presentationLayer2 = self.imageView2.layer.presentationLayer;
BOOL nowIntersecting = CGRectIntersectsRect([presentationLayer1 frame], [presentationLayer2 frame]);
// I'll keep track of whether the two views were intersected in a class property,
// and therefore only display a message if the intersected state changes.
if (nowIntersecting != self.wasIntersected)
{
if (nowIntersecting)
NSLog(#"imageviews now intersecting");
else
NSLog(#"imageviews no longer intersecting");
self.wasIntersected = nowIntersecting;
}
}
By the way, you may need to add Quartz 2D, the QuartzCore.framework, to your project. See the Project Editor Help.
Related
I am trying to get a UIView to expand using an animation block, which works perfectly. However, I want a UILabel to start at 0 and every 0.01 seconds to add 1 until it gets to 100.
I created a thread after the animation to accomplish this and it works but it causes the animation I setup to do nothing.
I have tried many different things but have had no luck. What would be the best way to accomplish this?
My simplest attempt with the same result as all the others:
[UIView animateWithDuration:1 animations:^{
_lView.frame = CGRectMake(_lView.frame.origin.x,_lView.frame.origin.y+_lView.frame.size.height,_lView.frame.size.width,-500);
}];
[[[NSThread alloc]initWithTarget:self selector:#selector(startcounting) object:nil]start];
-(void)startcounting{
for(int x=0; x<100; x++){
[NSThread sleepForTimeInterval:0.01];
++_mcount;
dispatch_async(dispatch_get_main_queue(), ^{
_cLabel.text = [NSString stringWithFormat:#"%i",_mcount];
});
}
}
There are two issues:
Regarding the animation of the frame while simultaneously changing the label, the issue is that changing the label's text causes the constraints to be reapplied. The correct solution is to not animate by changing the frame, but rather by changing the constraints that dictate the frame and then calling layoutIfNeeded in the animation block. See Animating an image view to slide upwards
Regarding the animating of the label:
You have no assurances that updates can be processed that quickly. (In fact you should plan on never realizing more than 60 fps.)
Even if you were able to reduce the frequency of the updates sufficiently, you are never assured that they'll be processed at a constant rate (something else could always block the main thread for a few milliseconds, yielding an inconsistent incrementing of the numbers).
So, you should instead use a timer (or better a "display link", which is like a timer, but coordinated to be called when the screen is ready to be refreshed), calculate the time elapsed (that way the counter won't be affected by other things going on, but the value will go from 0 to 100 quickly enough that it will yield the effect you were looking for), and update the label accordingly. For example:
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval startTime;
#property (weak, nonatomic) IBOutlet UILabel *label;
#end
#implementation ViewController
- (void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink {
CFTimeInterval elapsed = CACurrentMediaTime() - self.startTime;
if (elapsed < 1) {
self.label.text = [NSString stringWithFormat:#"%.0f", elapsed * 100.0];
} else {
self.label.text = #"100";
[self stopDisplayLink];
}
}
Thus, combining these two points, just adjust the constraint (after adding the outlet to the constraint) and call startDisplayLink (from the main thread) and your label will be updated from 0 to 100 over the span of one second as the view animates:
[self startDisplayLink];
self.topConstraint.constant += self.lView.frame.size.height;
[UIView animateWithDuration:1.0 animations:^{
[self.view layoutIfNeeded];
}];
_lView.frame = CGRectMake(_lView.frame.origin.x,[[UIScreen mainScreen] bounds].size.height,_lView.frame.size.width,_lView.frame.size.height);
[[[NSThread alloc]initWithTarget:self selector:#selector(startcounting) object:nil]start];
/*
Comment this code
[UIView animateWithDuration:1 animations:^{
_lView.frame = CGRectMake(_lView.frame.origin.x,_lView.frame.origin.y+_lView.frame.size.height,_lView.frame.size.width,-500);
}];
*/
Modify this method
-(void)startcounting{
int initialY = _lView.frame.origin.y;
for(int x=0; x<=100; x++){
[NSThread sleepForTimeInterval:0.01];
dispatch_async(dispatch_get_main_queue(), ^{
if (initialY>=_lView.frame.origin.y-(x*0.03))//adjust 0.03 as you required
{
_lView.frame = CGRectMake(_lView.frame.origin.x,_lView.frame.origin.y-(x*0.03),_lView.frame.size.width,_lView.frame.size.height);//adjust 0.03 as you required
}
else
{
_lView.frame = CGRectMake(_lView.frame.origin.x,initialY,_lView.frame.size.width,_lView.frame.size.height);
}
_cLabel.text = [NSString stringWithFormat:#"%i",x];
});
}
}
I finally figured it out. I should have seen this before.
First I figured it was an issue with the constraints so I removed the constraints from the UIView I am updating. This was necessary but did not fix the issue.
The entire view controller still has its own constraints and I cannot remove these as they are needed. So what I did was added a new UIView with constraints and put the UIView to be animated on this new UIView.
This fixed the problem because the animated UIView is no longer responsible for any constraints or responsible for being affected by any other constraints.
What I still do not understand is how updating the UILabel simultaneously causes the animation to fail when constraints are involved, but work perfectly fine when the UILabel code is removed. However though this did solve the issue.
There are a lot of similar questions but they all differ from this one.
I have UIScrollView which I could both scroll and stop programmatically.
I scroll via the following code:
[UIView animateWithDuration:3
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{ [self.scrollView scrollRectToVisible:newPageRect animated:NO]; }];
And I don't know how to stop it at all. In all the cases it won't stop or will stop but it also jumps to newPageRect (for example in the case of removeAllAnimations).
Could you suggest how to stop it correctly? Should I possibly change my code for scrolling to another one?
I think this is something you best do yourself. It may take you a few hours to create a proper library to animate data but in the end it can be very rewarding.
A few components are needed:
A time bound animation should include either a CADispalyLink or a NSTimer. Create a public method such as animateWithDuration: which will start the timer, record a current date and set the target date. Insert a floating value as a property which should then be interpolated from 0 to 1 through date. Will most likely look something like that:
- (void)onTimer {
NSDate *currentTime = [NSDate date];
CGFloat interpolation = [currentTime timeIntervalSinceDate:self.startTime]/[self.targetTime timeIntervalSinceDate:self.startTime];
if(interpolation < .0f) { // this could happen if delay is implemented and start time may actually be larger then current
self.currentValue = .0f;
}
else if(interpolation > 1.0f) { // The animation has ended
self.currentValue = 1.0f;
[self.displayLink invalidate]; // stop the animation
// TODO: notify owner that the animation has ended
}
else {
self.currentValue = interpolation;
// TODO: notify owner of change made
}
}
As you can see from the comments you should have 2 more calls in this method which will notify the owner/listener to the changes of the animation. This may be achieved via delegates, blocks, invocations, target-selector pairs...
So at this point you have a floating value interpolating between 0 and 1 which can now be used to interpolate the rect you want to be visible. This is quite an easy method:
- (CGRect)interpolateRect:(CGRect)source to:(CGRect)target withScale:(CGFloat)scale
{
return CGRectMake(source.origin.x + (target.origin.x-source.origin.x)*scale,
source.origin.y + (target.origin.y-source.origin.y)*scale,
source.size.width + (target.size.width-source.size.width)*scale,
source.size.height + (target.size.height-source.size.height)*scale);
}
So now to put it all together it would look something like so:
- (void)animateVisibleRectTo:(CGRect)frame {
CGRect source = self.scrollView.visibleRect;
CGRect target = frame;
[self.animator animateWithDuration:.5 block:^(CGFloat scale, BOOL didFinish) {
CGRect interpolatedFrame = [Interpolator interpolateRect:source to:target withScale:scale];
[self.scrollView scrollRectToVisible:interpolatedFrame animated:NO];
}];
}
This can be a great system that can be used in very many systems when you want to animate something not animatable or simply have a better control over the animation. You may add the stop method which needs to invalidate the timer or display link and notify the owner.
What you need to look out for is not to create a retain cycle. If a class retains the animator object and the animator object retains the listener (the class) you will create a retain cycle.
Also just as a bonus you may very easily implement other properties of the animation such as delay by computing a larger start time. You can create any type of curve such as ease-in, ease-out by using an appropriate function for computing the currentValue for instance self.currentValue = pow(interpolation, 1.4) will be much like ease-in. A power of 1.0/1.4 would be a same version of ease-out.
I have two UIViews with two different animations using UIView.animateWithDuration. The first animation starts right away, the second starts after a 0.5s delay.
How do I draw and animate a line between them like the below example:
My first attempt was to draw the line as a CGPath and then animate it using CABasicAnimation. This works if the two views (or shapes in that test) animates at the same time, not when the second animation has a delayed start.
I've then been looking into grabbing the values of the UIView frame positions on a continuous basis. That would enable me to redraw my line on each animation frame but I couldn't find any way of doing that either.
So... How do I achieve this?
CADisplayLink is probably what you are looking for.
Add an update method to your class and perform the animations there:
- (void)update {
// animate view 1
CGRect frame = view1.frame;
frame.origin.y += 1;
view1.frame = frame;
// animate view 2
// draw the line/animate another view
}
When you want to start the animation, do:
displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(update)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
Once the animation is complete remove the displayLink from the run loop.
While i'm implementing a game, i'm just front of a matter, whose really easy i think, but don't know how to fix it. I'm a bit new in objective-c as you could see with my reputation :(
The problem is, i have an animation, which works correctly. Here is the code :
CABasicAnimation * bordgauche = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
bordgauche.fromValue = [NSNumber numberWithFloat:0.0f];
bordgauche.toValue = [NSNumber numberWithFloat:749.0f];
bordgauche.duration = t;
bordgauche.repeatCount = 1;
[ImageSuivante.layer addAnimation:bordgauche forKey:#"bordgauche"];
And i want to get the current position of my image. So i use :
CALayer *currentLayer = (CALayer *)[ImageSuivante.layer presentationLayer];
currentX = [(NSNumber *)[currentLayer valueForKeyPath:#"transform.translation.x"] floatValue];
But i don't get it instantly. I get one time "Current = 0.0000", which is the starting value, when i use a nslog to print it, but not the others after.
I don't know how to get the instant position of my image, currentX, all the time.
I expect i was understable.
Thanks for your help :)
You can get the value from your layer's presentation layer.
CGPoint currentPos = [ImageSuivante.layer.presentationLayer position];
NSLog(#"%f %f",currentPos.x,currentPos.y);
I think you have 3 options here (pls comment if more exist):
option1: split your first animation into two and when the first half ends start the second half of the animation plus the other animation
...
bordgauche.toValue = [NSNumber numberWithFloat:749.0f / 2];
bordgauche.duration = t/2;
bordgauche.delegate = self // necessary to catch end of anim
[bordgauche setValue:#"bordgauche_1" forKey: #"animname"]; // to identify anim if more exist
...
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
if ([theAnimation valueForKey: #"animname"]==#"bordgauche_1") {
CABasicAnimation * bordgauche = [CABasicAnimation
animationWithKeyPath:#"transform.translation.x"];
bordgauche.fromValue = [NSNumber numberWithFloat:749.0f / 2];
bordgauche.toValue = [NSNumber numberWithFloat:749.0f];
bordgauche.duration = t/2;
bordgauche.repeatCount = 1;
[ImageSuivante.layer addAnimation:bordgauche forKey:#"bordgauche_2"];
// plus start your second anim
}
option2: setup a NSTimer or a CADisplayLink (this is better) callback and check continuously the parameters of your animating layer. Test the parameters for the required value to trigger the second anim.
displayLink = [NSClassFromString(#"CADisplayLink") displayLinkWithTarget:self selector:#selector(check_ca_anim)];
[displayLink setFrameInterval:1];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
... or
animationTimer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(1.0 / 60.0) target:self selector:#selector(check_ca_anim) userInfo:nil repeats:TRUE];
[[NSRunLoop mainRunLoop] addTimer:animationTimer forMode:NSRunLoopCommonModes];
- (void) check_ca_anim {
...
CGPoint currentPosition = [[ImageSuivante.layer presentationLayer] position];
// test it and conditionally start something
...
}
option3: setup a CADisplayLink (can be called now as "gameloop") and manage the animation yourself by calculating and setting the proper parameters of the animating object. Not knowing what kind of game you would like to create I would say game loop might be useful for other game specific reasons. Also here I mention Cocos2d which is a great framework for game development.
You can try getting the value from your layer's presentation layer, which should be close to what is being presented on screen:
[ [ ImageSuivante.layer presentationLayer ] valueForKeyPath:#"transform.translation.x" ] ;
I would add one option to what #codedad pointed out. What's interesting, it gives a possibility of getting instant values of your animating layer precisely (you can see CALayer's list of animatable properties) and it's very simple.
If you override method
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
in your UIView class (yep, you'd need to implement a custom class derived from UIView for your "ImageSuivante" view), then the parameter layer which is passed there would give you exactly what you need. It contains precise values used for drawing.
E.g. in this method you could update a proper instant variable (to work with later) and then call super if you don't do drawing with you hands:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
self.instantOpacity = layer.opacity;
self.instantTransform = layer.transform;
[super drawLayer:layer inContext:context];
}
Note that layer here is generally not the same object as self.layer (where self is your custom UIView), so e.g. self.layer.opacity will give you the target opacity, but not the instant one, whereas self.instantOpacity after the code above will contain the instant value you want.
Just be aware that this is a drawing method and it may be called very frequently, so no unnecessary calculations or any heavy operations there.
The source of this idea is Apple's Custom Animatable Property project.
I need to closely monitor the scale of the scroll view so that I can update the content view's elements (a subview managing multiple CALayers) according to the scroll view's animated zoom.
On iOS 3.1, everything works as expected, I use zoomToRect:animated: and the scrollViewDidScroll: message of the UIScrollViewDelegate gets called repeatedly while the animation takes place, letting me update the subview elements according to actual zoom.
The same code on iOS 4.0 does not exibit the same behavior. When I call zoomToRect:animated:, the delegates (scrollViewDidScroll: and scrollViewDidZoom) only get called once, which makes my sub elements desinchronized until the animation is finished. In fact, the sub elements immediately jump and then get caught up by the zoom animation until everything is in the correct place. It's as if the animation is not picking up the modifications on the subviews CALayers.
I have tried animating manually with animation blocks, but the situation is the same, no progressive callback calls. I have also tried KVO, but it is not clear to me how I would tap into a UIScrollView-managed animation.
Is there a workaround on iOS 4 that would allow me to push the scale value to my subviews as the UIScrollView scale is animated?
Brutal but works.
Define some properties:
#property (nonatomic, strong) UIView *zoomedView;
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CGFloat currentScale;
Inform UIScrollView which UIView is going to be zoomed:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.zoomedView;
}
Register for a CADisplayLink ticks. It runs Core Animation as well, so it will be kicked on same intervals:
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkTick)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
Check zoomedView's presentationLayer for current values.
- (void)displayLinkTick
{
CALayer *zoomedLayer = self.zoomedView.layer.presentationLayer;
CGFloat scale = zoomedLayer.transform.m11;
if (scale != self.currentScale) {
self.currentScale = scale; // update property
// the scale has changed!
}
}
In IOS 4.0, animations are done by the OS - I assume to make use of GPU based hardware acceleration as much as possible. As a disadvantage of that, it becomes harder to animate a values that is derived from another (animated) value. As in your case, the positions of the subviews that depend on the zoom level of the UIScrollView.
In order to make that happen, you should setup the animation of the subviews to go in parallel with the animation of the zooming. Try something like:
[UIView animateWithDuration:0.5 delay:0
options:UIViewAnimationOptionLayoutSubviews
animations:^{
theScrollView.zoomScale = zoomScale;
// layout the subviews here
} completion:^(BOOL finished) {}];
This should set the frame properties of the subviews from within the same animation context, and therefore, they should be animated together by the OS.
See also the answers to this question: How to make UIScrollView send scrollViewDidScroll messages during animations
Is it viable to update all the subviews within an animation block (or begin/commitAnimation)?
Something like:
[self zoomToRect:zoomRect animated:YES];
[UIView animateWithDuration:1.0
animations:^ {
// change your subviews
}
];
Register for KVO of the zoomScale property of the UIScrollView after you call zoomToRect:animated:.
[scrollView addObserver:self
forKeyPath:#"zoomScale"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
Then, implement this method:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:#"zoomScale"]) {
// use the new zoomScale value stored
// in the NSKeyValueChangeNewKey change
// dictionary key to resize your subviews
}
// be sure to call the super implementation
// if the superclass implements it
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
Check out the KVO documentation.