I have a Core Animation image on boxLayer and I'm duplicating it, changing the action of and shifting the position of the 2nd (boxLayer2) so that someone can choose between the 2.
I want the user to be able to tap the image for boxLayer and the boxLayer2 image does nothing but boxLayer moves (I didn't include my animation code beyond receiving the touch) and viceversa.
I cannot get an if statement to work. I've tried multiple variations self.layer == boxLayer or CALayer == boxlayer ... sublayer is an array so that's out. Any help/explanation as I know I'm missing something would be greatly appreciated.
Thanks!
UIView *BounceView is declared in the VC
In BounceView I have 2 CALayers declared: boxlayer & boxlayer2
BounceView.m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
// Create the new layer object
boxLayer = [[CALayer alloc] init];
boxLayer2 = [[CALayer alloc] init];
// Give it a size
[boxLayer setBounds:CGRectMake(0.0, 0.0, 185.0, 85.0)];
[boxLayer2 setBounds:CGRectMake(0.0, 0.0, 185.0, 85.0)];
// Give it a location
[boxLayer setPosition:CGPointMake(150.0, 140.0)];
[boxLayer2 setPosition:CGPointMake(150.0, 540.0)];
// Create a UIImage
UIImage *layerImage = [UIImage imageNamed:#"error-label.png"];
UIImage *layerImage2 = [UIImage imageNamed:#"error-label.png"];
// Get the underlying CGImage
CGImageRef image = [layerImage CGImage];
CGImageRef image2 = [layerImage2 CGImage];
// Put the CGImage on the layer
[boxLayer setContents:(__bridge id)image];
[boxLayer2 setContents:(__bridge id)image2];
// Let the image resize (without changing the aspect ratio)
// to fill the contentRect
[boxLayer setContentsGravity:kCAGravityResizeAspect];
[boxLayer2 setContentsGravity:kCAGravityResizeAspect];
// Make it a sublayer of the view's layer
[[self layer] addSublayer:boxLayer];
[[self layer] addSublayer:boxLayer2];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event
{
if (CAlayer == boxLayer)
{
// do something
}
else
{
// do something else
}
}
It looks to me like you are trying to know what layer the user tapped on inside touched began and that this is your problem.
How to find out what layer was tapped
CALayer has an instance method - (CALayer *)hitTest:(CGPoint)thePoint that
Returns the farthest descendant of the receiver in the layer hierarchy (including itself) that contains a specified point.
So to find out what layer you tapped you should do something like
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event {
UITouch *anyTouch = [[event allTouches] anyObject];
CGPoint pointInView = [anyTouch locationInView:self];
// Now you can test what layer that was tapped ...
if ([boxLayer hitTest:pointInView]) {
// do something with boxLayer
}
// the rest of your code
}
This works because hitTest will return nil if the point is outside the layer's bounds.
David Rönnqvist's post tells you how to use hitTest on the layer to figure out which layer was touched. That should work. I would code that method slightly differently, though. I would have my view's layer include boxLayer and boxLayer2 as sub-layers, and then send the hitTest method to the parent layer. It would then return the layer that contains the touch.
It would be much simpler, though, if you use separate views, each with a layer that contains your content. Then you can use gesture recognizers on each view, and higher level Cocoa Touch code rather than CA code to manage taps. Cleaner and easier to maintain.
Related
I have a UIButton and i want it to display a triangle. Is there a function to make it a triangle? Since im not using a UIView class im not sure how to make my frame a triangle.
ViewController(m):
- (IBAction)makeTriangle:(id)sender {
UIView *triangle=[[UIView alloc] init];
triangle.frame= CGRectMake(100, 100, 100, 100);
triangle.backgroundColor = [UIColor yellowColor];
[self.view addSubview: triangle];
Do i have to change my layer or add points and connect them to make a triangle with CGRect?
If im being unclear or not specific add a comment. Thank you!
A button is a subclass of UIView, so you can make it any shape you want using a CAShape layer. For the code below, I added a 100 x 100 point button in the storyboard, and changed its class to RDButton.
#interface RDButton ()
#property (strong,nonatomic) UIBezierPath *shape;
#end
#implementation RDButton
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.titleEdgeInsets = UIEdgeInsetsMake(30, 0, 0, 0); // move the title down to make it look more centered
self.shape = [UIBezierPath new];
[self.shape moveToPoint:CGPointMake(0,100)];
[self.shape addLineToPoint:CGPointMake(100,100)];
[self.shape addLineToPoint:CGPointMake(50,0)];
[self.shape closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.shape.CGPath;
shapeLayer.fillColor = [UIColor yellowColor].CGColor;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.shape containsPoint:[touches.anyObject locationInView:self]])
[super touchesBegan:touches withEvent:event];
}
The touchesBegan:withEvent: override restricts the action of the button to touches within the triangle.
A view's frame is always a rect, which is a rectangle. Even if you apply a transform to it so it no longer looks like a rectangle, the view.frame property will still be a rectangle -- just the smallest possible rectangle that contains the new shape you have produced.
So if you want your UIButton to look like a triangle, the simplest solution is probably to set its type to UIButtonTypeCustom and then to set its image to be a png which shows a triangle and is transparent outside of the triangle.
Then the UIButton itself will actually be rectangle, but will look like a triangle.
If you want to get fancy, you can also customize touch delivery so that touches on the transparent part of the PNG are not recognized (as I believe they would be by default), but that might be a bit trickier.
When researching "How do I detect a touch event on a moving UIImageView?" I've come across several answers and I tried to implement them to my app. Nothing I've come across seems to work. I'll explain what I'm trying to do then post my code. Any thoughts, suggestions, comments or answers are appreciated!
My app has several cards floating across the screen from left to right. These cards are various colors and the object of the game is the drag the cards down to their similarly colored corresponding container. If the user doesn't touch and drag the cards fast enough, the cards will simply drift off the screen and points will be lost. The more cards contained in the correct containers, the better the score.
I've written code using core animation to have my cards float from the left to right. This works. However when attempting to touch a card and drag it toward it's container, it isn't correctly detecting that I'm touching the UIImageView of the card.
To test if my I'm properly implementing the code to move a card, I've also written some code allows movement for a non-moving card. In this case my touch is being detected and acting accordingly.
Why can I only interact with stationary cards? After researching this quite a bit it seems that the code:
options:UIViewAnimationOptionAllowUserInteraction
is the key ingredient to get my moving UIImages to be detected. However I tried this doesn't seem to have any effect.
I another key thing that I may be doing wrong is not properly utilizing the correct presentation layer. I've added code like this to my project and I also only works on non-moving objects:
UITouch *t = [touches anyObject];
UIView *myTouchedView = [t view];
CGPoint thePoint = [t locationInView:self.view];
if([_card.layer.presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Card!");
}
else{
NSLog(#"backgound touched");
}
After trying these types of things I'm getting stuck. Here is my code to understand this a bit more completely:
#import "RBViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface RBViewController ()
#property (nonatomic, strong) UIImageView *card;
#end
#implementation RBViewController
- (void)viewDidLoad
{
srand(time (NULL)); // will be used for random colors, drift speeds, and locations of cards
[super viewDidLoad];
[self setOutFirstCardSet]; // this sends out 4 floating cards across the screen
// the following creates a non-moving image that I can move.
_card = [[UIImageView alloc] initWithFrame:CGRectMake(400,400,100,100)];
_card.image = [UIImage imageNamed:#"goodguyPINK.png"];
_card.userInteractionEnabled = YES;
[self.view addSubview:_card];
}
the following method sends out cards from a random location on the left side of the screen and uses core animation to drift the card across the screen. Notice the color of the card and the speed of the drift will be randomly generated as well.
-(void) setOutFirstCardSet
{
for(int i=1; i < 5; i++) // sends out 4 shapes
{
CGRect cardFramei;
int startingLocation = rand() % 325;
CGRect cardOrigini = CGRectMake(-100,startingLocation + 37, 92, 87);
cardFramei.size = CGSizeMake(92, 87);
CGPoint origini;
origini.y = startingLocation + 37;
origini.x = 1200;
cardFramei.origin = origini;
_card.userInteractionEnabled = YES;
_card = [[UIImageView alloc] initWithFrame:cardOrigini];
int randomColor = rand() % 7;
if(randomColor == 0)
{
_card.image = [UIImage imageNamed:#"goodguy.png"];
}
else if (randomColor == 1)
{
_card.image = [UIImage imageNamed:#"goodguyPINK.png"];
}
else if (randomColor == 2)
{
_card.image = [UIImage imageNamed:#"goodGuyPURPLE.png"];
}
else if (randomColor == 3)
{
_card.image = [UIImage imageNamed:#"goodGuyORANGE.png"];
}
else if (randomColor == 4)
{
_card.image = [UIImage imageNamed:#"goodGuyLightPINK.png"];
}
else if (randomColor == 5)
{
_card.image = [UIImage imageNamed:#"goodGuyBLUE.png"];
}
else if (randomColor == 6)
{
_card.image = [UIImage imageNamed:#"goodGuyGREEN.png"];
}
_card.userInteractionEnabled = YES; // this is also written in my viewDidLoad method
[[_card.layer presentationLayer] hitTest:origini]; // not really sure what this does
[self.view addSubview:_card];
int randomSpeed = rand() % 20;
int randomDelay = rand() % 2;
[UIView animateWithDuration:randomSpeed + 10
delay: randomDelay + 4
options:UIViewAnimationOptionAllowUserInteraction // here is the method that I thought would allow me to interact with the moving cards. Not sure why I can't
animations: ^{
_card.frame = cardFramei;
}
completion:NULL];
}
}
notice the following method is where I put CALayer and hit test information. I'm not sure if I'm doing this correctly.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
UIView *myTouchedView = [t view];
CGPoint thePoint = [t locationInView:self.view];
thePoint = [self.view.layer convertPoint:thePoint toLayer:self.view.layer.superlayer];
CALayer *theLayer = [self.view.layer hitTest:thePoint];
if([_card.layer.presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Shape!"); // This only logs when I touch a non-moving shape
}
else{
NSLog(#"backgound touched"); // this logs when I touch the background or an moving shape.
}
if(myTouchedView == _card)
{
NSLog(#"Touched a card");
_boolHasCard = YES;
}
else
{
NSLog(#"Didn't touch a card");
_boolHasCard = NO;
}
}
I want the following method to work on moving shapes. It only works on non-moving shapes. Many answers say to have the touch ask which class the card is from. As of now all my cards on of the same class (the viewController class). When trying to have the cards be their own class, I was having trouble having that view appear on my main background controller. Must I have various cards be from different classes for this to work, or can I have it work without needing to do so?
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
if([touch view]==self.card)
{
CGPoint location = [touch locationInView:self.view];
self.card.center=location;
}
}
This next method resets the movement of a card if the user starts moving it and then lifts up on it.
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if(_boolHasCard == YES)
{
[UIView animateWithDuration:3
delay: 0
options:UIViewAnimationOptionAllowUserInteraction
animations: ^{
CGRect newCardOrigin = CGRectMake(1200,_card.center.y - 92/2, 92, 87);
_card.frame = newCardOrigin;
}
completion:NULL];
}
}
#end
The short answer is, you can't.
Core Animation does not actually move the objects along the animation path. They move the presentation layer of the object's layer.
The moment the animation begins, the system thinks the object is at it's destination.
There is no way around this if you want to use Core Animation.
You have a couple of choices.
You can set up a CADisplayLink on your view controller and roll your own animation, where you move the center of your views by a small amount on each call to the display link. This might lead to poor performance and jerky animation if you're animating a lot of objects however.
You can add a gesture recognizer to the parent view that contains all your animations, and then use layer hit testing on the paren't view's presentation view to figure out which animating layer got tapped, then fetch that layer's delegate, which will be the view you are animating. I have a project on github that shows how to do this second technique. It only detects taps on a single moving view, but it will show you the basics: Core Animation demo project on github.
(up-votes always appreciated if you find this post helpful)
It looks to me that your problem is really with just an incomplete understanding of how to convert a point between coordinate spaces. This code works exactly as expected:
- (void)viewDidLoad
{
[super viewDidLoad];
CGPoint endPoint = CGPointMake([[self view] bounds].size.width,
[[self view] bounds].size.height);
CABasicAnimation *animation = [CABasicAnimation
animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:[[_imageView layer] position]];
animation.toValue = [NSValue valueWithCGPoint:endPoint];
animation.duration = 30.0f;
[[_imageView layer] addAnimation:animation forKey:#"position"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint thePoint = [t locationInView:self.view];
thePoint = [[_imageView layer] convertPoint:thePoint
toLayer:[[self view] layer]];
if([[_imageView layer].presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Shape!");
}
else{
NSLog(#"backgound touched");
}
}
Notice the line in particular:
thePoint = [[_imageView layer] convertPoint:thePoint
toLayer:[[self view] layer]];
When I tap on the layer image view while it's animating, I get "You touched a Shape!" in the console window and I get "background touched" when I tap around it. That's what you're wanting right?
Here's a sample project on Github
UPDATE
To help with your follow up question in the comments, I've written the touchesBegan code a little differently. Imagine that you've add all of your image views to an array (cleverly named imageViews) when you create them. You would alter your code to look something like this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint thePoint = [t locationInView:self.view];
for (UIImageView *imageView in [self imageViews]) {
thePoint = [[imageView layer] convertPoint:thePoint
toLayer:[[self view] layer]];
if([[imageView layer].presentationLayer hitTest:thePoint]) {
NSLog(#"Found it!!");
break; // No need to keep iterating, we've found it
} else{
NSLog(#"Not this one!");
}
}
}
I'm not sure how expensive this is, so you may have to profile it, but it should do what you're expecting.
The following code produces an animation of an image of a shape from the top of the screen and it drifts downward using core animation. When the user taps, it will log whether the user tapped the image (the shape) or if they missed the shape and therefore touched the background. This seems to work fine. However what about when I add in other images of shapes? I'm looking for suggestions as to how to build onto this code to allow for more detailed information to be logged.
Let's say I want to programmatically add in a UIImage of triangle, a UIImage of a square, and a UIImage of a circle. I want all three images to start drifting from top to bottom. They may even overlap each other as they transition. I want to be able to log "You touched the square!" or whatever the appropriate shape I've touched. I want to be able to do so even if the square is positioned in between the triangle and the circle but part of the square is showing so I can tap it. (This example shows I'm not just wanting to interact with the top-most layer)
How do I tweak this code to programmatically add in different UIImages (various shape images perhaps) and be able to log which shape I'm touching?
- (void)viewDidLoad
{
[super viewDidLoad];
CGPoint endPoint = CGPointMake([[self view] bounds].size.width,
[[self view] bounds].size.height);
CABasicAnimation *animation = [CABasicAnimation
animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:[[_imageView layer] position]];
animation.toValue = [NSValue valueWithCGPoint:endPoint];
animation.duration = 30.0f;
[[_imageView layer] addAnimation:animation forKey:#"position"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint thePoint = [t locationInView:self.view];
thePoint = [[_imageView layer] convertPoint:thePoint toLayer:[[self view] layer]];
if([[_imageView layer].presentationLayer hitTest:thePoint])
{
NSLog(#"You touched a Shape!");
// for now I'm just logging this information. Eventually I want to have the shape follow my figure as I move it to a new location. I want everything else to continue animating but I when I touch a particular shape I want to have complete control on repositioning that specific shape. That's just some insight beyond the scope of this question. However feel free to comment about this if you have suggestions.
}
else{
NSLog(#"backgound touched");
}
}
I'm thinking the answer to this may have something to do with looping the the various subviews. Look at how I'm thinking I might change the -touchesBegan method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *t = [t anyObject];
CGPoint thePoint = [t locationInView:self.view];
for (UIView *myView in viewArray) {
if (CGRectContainsPoint(myView.frame, thePoint)) {....
Notice here I set up a viewArray and have put all my subviews in the viewArray. Is this something I should be using? Or perhaps something like the following if I was going to loop through my layers:
for(CALayer *mylayer in self.view.layer.sublayers)
No matter how much I try looping through my views and or layers I can't seem to get this to work. I feel like I may just be missing something obvious...
I think that the culprit is the line where you change the coordinate system for thePoint. It should probably read convertPoint:fromLayer: as prior to the execution of that line, your point is in the coordinate system of self.view and I'm assuming that you would like it to be in that of the imaveView. Alternately, you might skip that line altogether and call [t locationInView:_imageView] instead.
I want to do a similar thing to what the app Scalar did, where they made a capability to drag from a point onto the notepad to paste the number to the location they draged the dot too. What I'm really interested in is the line that stays connected to the dot and your finger as shown here:
My problem is that I don't really know what this is called, so I am having trouble searching for how I would do this. Does anyone know what this would be called, have any tutorials they have came across on this subject? And even better if you have some code for me to look at would be awesome.
Thanks.
This example of a UIView that draws lines when a finger is dragged on it and detects the first view to be touched should help you start.
//this goes in the header file called "UILineView.h"
#import <UIKit/UIKit.h>
#interface UILineView : UIView
#end
//this in the implementation file called "UILineView.m"
#import "UILineView.h"
#implementation UILineView
{
CGPoint _originOfTouchPoint; // your fist touch detected in touchesBegan: method
CGPoint _currentFingerPositionPoint; // the position you have dragged your finger to
CGFloat _strokeWidth; // the width of the line you wish to draw
id _touchStartedObject; // the object(UIView) that the first touch was detected on
}
// If you use Interface Builder to design your interface, Objects in a nib file are reconstituted and then initialized using
// their initWithCoder: method
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (self) {
// Initialization code
_originOfTouchPoint = CGPointMake( 0.0, 0.0 );
_currentFingerPositionPoint = CGPointMake( 100.0, 100.0 );
_strokeWidth = 2.0;
}
return self;
}
/*
// Use initWithFrame if you are not loding the UIView from a nib file
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
_originOfTouchPoint = CGPointMake( 0.0, 0.0 );
_currentFingerPositionPoint = CGPointMake( 100.0, 100.0 );
_strokeWidth = 2.0;
}
return self;
}
*/
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor( context, [UIColor blueColor].CGColor );
CGContextSetLineWidth( context, _strokeWidth );
// fisrt point of line
CGContextMoveToPoint( context, _originOfTouchPoint.x, _originOfTouchPoint.y );
// last point of line
CGContextAddLineToPoint( context, _currentFingerPositionPoint.x, _currentFingerPositionPoint.y );
// draw the line
CGContextStrokePath( context );
}
#pragma mark touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// get starting point and first view touched (if you need to send that view messages)
_originOfTouchPoint = [[touches anyObject] locationInView:self];
_touchStartedObject = [[touches anyObject] view];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint movedToPoint = [[touches anyObject] locationInView:self];
// if moved to a new point redraw the line
if ( CGPointEqualToPoint( movedToPoint, _currentFingerPositionPoint ) == NO )
{
_currentFingerPositionPoint = movedToPoint;
// calls drawRect: method to show updated line
[self setNeedsDisplay];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// reset values
_originOfTouchPoint = CGPointZero;
_currentFingerPositionPoint = CGPointZero;
_touchStartedObject = nil;
}
#end
It will involving subclassing UIView and implemeting the touchesBegan and touchesMoved method and drawing a line in your view subclass drawRect method from the initial touch to your current touch.
heres a previous question that will help How do I draw a line on the iPhone?
you will just need to change the following so that the coordinates are those of your initial touch and current touch that you obtain from the touches methods
CGContextMoveToPoint(c, 5.0f, 5.0f);
CGContextAddLineToPoint(c, 50.0f, 50.0f);
then calling [self setNeedsDisplay]; in the touchesMoved method will redraw the line to follow your finger as you move.
Then implement touchesEnded for code when the finger is lifted off.
Hoope it helps!
A UIGestureRecognizer can do this without confusing the touch and drag logic with a view. I have used them for editing shapes on maps, long press dragging, etc. See this tutorial.
I am using CALayers animation in my code.
Below is my code
CALayer *backingLayer = [CALayer layer];
backingLayer.contentsGravity = kCAGravityResizeAspect;
// set opaque to improve rendering speed
backingLayer.opaque = YES;
backingLayer.backgroundColor = [UIColor whiteColor].CGColor;
backingLayer.frame = CGRectMake(0, 0, templateWidth, templateHeight);
[backingLayer addSublayer:view.layer];
CGFloat scale = [[UIScreen mainScreen] scale];
CGSize size = CGSizeMake(backingLayer.frame.size.width*scale, backingLayer.frame.size.height*scale);
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[backingLayer renderInContext:context];
templateImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This backingLayer has many sublayers added like this, and this view is my subView.
But now how can i get the events of the view in the respective UIViews since i have added them as sublayers , i am trying to achieve something like the Flipboard application, where they have the page navigation and click events even though they are sub layers.
The point of CALayers is that they are light weight, specifically not having the event handling overhead. That is what UIView's are for. Your options are to either convert your code to using UIViews for event tracking, or write your own event passing code. For the second one, basically, you would have your containing UIView do a bunch of "is point in rect" queries for each sub-layer's bounds, and pass the event to (a custom method in) the CALayer with the highest z-position.
As claireware mentioned, CALayers don't support event handling directly. However, you can capture the event in the UIView that contains the CALayer and send the UIView's implicit layer a 'hitTest' message to determine which layer was touched. For example:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
CALayer *target = [self.layer hitTest:location];
// target is the layer that was tapped
}
Here is more info about hitTest from Apple's documentation:
Returns the farthest descendant of the receiver in the layer hierarchy (including itself) that contains a specified point.
Return Value
The layer that contains thePoint, or nil if the point lies outside the receiver’s bounds rectangle.