How to detect which word touched with CoreText - cocoa-touch

I am using CoreText to layout a custom view. The next step for me is to detect which word is tapped on a touch event/gesture event. I have done research on this and found advise on how to custom label a url to receive touches - but nothing generic. Does any one have any idea on how to do this?
UPDATE:
Here is the code within my drawRect: method
self.attribString = [aString copy];
CGContextRef context = UIGraphicsGetCurrentContext();
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGMutablePathRef path = CGPathCreateMutable(); //1
CGPathAddRect(path, NULL, self.bounds );
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)aString); //3
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [aString length]), path, NULL);
CTFrameDraw(frame, context); //4
UIGraphicsPushContext(context);
frameRef = frame;
CFRelease(path);
CFRelease(framesetter);
Here is where I am trying to handle the touch event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
CGContextRef context = UIGraphicsGetCurrentContext();
CFArrayRef lines = CTFrameGetLines(frameRef);
for(CFIndex i = 0; i < CFArrayGetCount(lines); i++)
{
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGRect lineBounds = CTLineGetImageBounds(line, context);
NSLog(#"Line %ld (%f, %f, %f, %f)", i, lineBounds.origin.x, lineBounds.origin.y, lineBounds.size.width, lineBounds.size.height);
NSLog(#"Point (%f, %f)", point.x, point.y);
if(CGRectContainsPoint(lineBounds, point))
{
It seems that CTLineGetImageBounds is returning a wrong origin (the size seems correct) here is one example of the NSLog "Line 0 (0.562500, -0.281250, 279.837891, 17.753906)".

There is no "current context" in touchesEnded:withEvent:. You are not drawing at this point. So you can't call CTLineGetImageBounds() meaningfully.
I believe the best solution here is to use CTFrameGetLineOrigins() to find the correct line (by checking the Y origins), and then using CTLineGetStringIndexForPosition() to find the correct character within the line (after subtracting the line origin from point). This works best for simple stacked lines that run the entire view (such as yours).
Other solutions:
Calculate all the line rectangles during drawRect: and cache them. Then you can just do rectangle checks in touchesEnded:.... That's a very good solution if drawing is less common than tapping. If drawing is significantly more common than tapping, then that's a bad approach.
Do all your calculations using CTLineGetTypographicBounds(). That doesn't require a graphics context. You can use this to work out the rectangles.
In drawRect: generate a CGLayer with the current context and store it in an ivar. Then use the context from the CGLayer to calculate CTLineGetImageBounds(). The context from the CGLayer will be "compatible" with the graphics context you're using to draw.
Side note: Why are you calling UIGraphicsPushContext(context); in drawRect:? You're setting the current context to the current context. That doesn't make sense. And I don't see a corresponding UIGraphicsPopContext().

Related

Draw and Erase Lines with Touch

I have a background image (ImageBG) and a foreground image (ImageFG) in separate UIImageViews. They are the same size and, as the name implies, ImageBG is behind ImageFG so you can't see ImageBG.
If the user starts to "draw" on them, instead of a line appearing where the user touches the screen, I want that portion of ImageFG to become transparent and reveal ImageBG.
The closest thing I can think of is a scratch-and-win.
I know how to do the drawing on a graphics context, but when I draw a transparent line (alpha = 0), well... I get a transparent line on top of whatever is currently in my graphics context, so basically nothing is drawn. This is what I use to set the stroke color.
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1.0, 0.0, 0.0, 0.0)
What am I doing wrong? I know this is possible because there are apps out there that do it.
Any hints, tips or direction is appreciated.
So you have a layer that is some image like the grey stuff you want to scratch off, you write this image (in this example scratchImage which is a UIImageView and its UIImage .image )to the graphics context. You then stroke it with the blend mode set to kCGBlendModeClear then you save that image (with the cleared paths). This will reveal an image that you have underneath perhaps in another UIImageView. Note that in this instance, self is a UIView.
So outside touchesMoved create a variable to hold a CGPoint
CGPoint previousPoint;
CGPoint currentPoint;
Then in touchesBegan, set it to the previous point.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
previousPoint = [touch locationInView:self];
}
In touchesMoved, write the image to the context, stroke the image with the clear blend mode, save the image from the context, and set the previous point to the current point.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
currentPoint = [touch locationInView:self];
UIGraphicsBeginImageContext(self.frame.size);
//This may not be necessary if you are just erasing, in my case I am
//adding lines repeatedly to an image so you may want to experiment with moving
//this to touchesBegan or something. Which of course would also require the begin
//graphics context etc.
[scratchImage.image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
CGContextSaveGState(UIGraphicsGetCurrentContext());
CGContextSetShouldAntialias(UIGraphicsGetCurrentContext(), YES);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 5.0);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.25, 0.25, 0.25, 1.0);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, previousPoint.x, previousPoint.y);
CGPathAddLineToPoint(path, nil, currentPoint.x, currentPoint.y);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextAddPath(UIGraphicsGetCurrentContext(), path);
CGContextStrokePath(UIGraphicsGetCurrentContext());
scratchImage.image = UIGraphicsGetImageFromCurrentImageContext();
CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
previousPoint = currentPoint;
}

GL_LINES drawing erratically (iPad + OpenGL ES1)

I'm experiencing a really strange glitch in my ipad app. It's super simple: I just use the "touchesMoved" handler to draw a line between two points. Since I would like lines to stay on screen, I'm not calling "glClear" in my draw function, but for some reason some of the lines just drop out, and it appears completely random. Stranger yet, it works perfectly in the simulator. Does anybody have any insight into why this might be? I've included my touch and draw routines.
Many thanks!
Pete
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
NSSet *allTouches = [event allTouches];
switch ([allTouches count])
{
case 1:
{ //Single touch
} break;
case 2:
{ //Double Touch
UITouch *touch1 = [[allTouches allObjects] objectAtIndex:0];
CGPoint location1 = [touch1 locationInView: [touch1 view]];
UITouch *touch2 = [[allTouches allObjects] objectAtIndex:1];
CGPoint location2 = [touch2 locationInView: [touch2 view]];
[self drawLineWithStart:location1 end:location2];
} break;
default:
break;
}
}
- (void)drawLineWithStart:(CGPoint)start end:(CGPoint) end
{
[EAGLContext setCurrentContext:context];
glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer);
GLfloat lineVertices[] =
{
(start.x/(768.0/2.0)) - 1.0, -1.5 * ((start.y/(1024.0/2.0)) - 1.0),
(end.x/(768.0/2.0)) - 1.0, -1.5 * ((end.y/(1024.0/2.0)) - 1.0)
};
glDisable(GL_TEXTURE_2D);
// Render the vertex array
glVertexPointer(2, GL_FLOAT, 0, lineVertices);
glDrawArrays(GL_LINE_STRIP, 0, 2);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER_OES];
}
This code does a presentRenderBuffer in a double buffered context without a clear. This means its drawing to the one buffer, presenting it, drawing to the other buffer, presenting it, etc. As a result no one visible buffer has all of the line drawing and the swap (present) will show differences in the alternating buffers. The simulator double buffering scheme is different than the physical devices which explains the difference you're seeing.
Accumulate the lines in a data structure. Each frame do the clear then draw all of the lines.
Depending on the situation, you can also retain the backing. This will solve your issue here, but will cost you performance.
In your OpenGLES2DView.m change kEAGLDrawablePropertyRetainedBacking to YES. It will look like this:
eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
But to reiterate, it will cost performance, however I would say if you are trying to write a drawing application (from the looks of it) it might be what you are looking for. Since in drawing apps, it is not a good idea to rerun the logic to redraw every frame, this is a good solution.

Cocoa-touch - drawing with core graphics to a touch point

so i want to do something which seems pretty simple but is proving other wise, i want to just draw a square at the point that touch i registered. i cant seem to get it to work though. in touchesBegan i am calling a custom method called drawSquare that is sent a CGRect. i know i must be doing something simple wrong but i don't know enough about drawing primitives in xcode/cocoa-touch. any help would be greatly appreciated. also here is my code:
- (void)drawSquare:(CGRect)rect{
//Get the CGContext from this view
CGContextRef context = UIGraphicsGetCurrentContext();
//Draw a rectangle
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
//Define a rectangle
CGContextAddRect(context, rect);
//Draw it
CGContextFillPath(context);
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
// gets the coordinats of the touch with respect to the specified view.
CGPoint touchPoint = [touch locationInView:self];
CGRect rect = CGRectMake(touchPoint.x, touchPoint.y, 50, 50);
[self drawSquare:rect];
}
}
Don't try to do your drawing in your event methods. Update your list of what you want to draw, and send -setNeedsDisplay.

Simple way of using irregular shaped buttons

I've finally got my main app release (Tap Play MMO - check it out ;-) ) and I'm now working on expanding it.
To do this I need to have a circle that has four seperate buttons in it, these buttons will essentially be quarters. I've come to the conclusion that the circlular image will need to be constructed of four images, one for each quarter, but due to the necessity of rectangular image shapes I'm going to end up with some overlap, although the overlap will be transparent.
What's the best way of getting this to work? I need something really simple really, I've looked at this
http://iphonedevelopment.blogspot.com/2010/03/irregularly-shaped-uibuttons.html
Before but not yet succeeded in getting it to work. Anyone able to offer some advice?
In case it makes any difference I'll be deploying to a iOS 3.X framework (will be 4.2 down the line when 4.2 comes out for iPad)
Skip the buttons and simply respond to touches in your view that contains the circle.
Create a CGPath for each area that you want to capture touches, when your UIview receives a touch, check for membership inside the paths.
[Edited answer to show skeleton implementation details -- TomH]
Here's how I would approach the problem: (I haven't tested this code and the syntax may not be quite right, but this is the general idea)
1) Using PS or your favorite image creation application, create one png of the quarter circles. Add it to your XCode project.
2) Add a UIView to the UI. Set the UIView's layer's contents to the png.
self.myView = [[UIView alloc] initWithRect:CGRectMake(10.0, 10.0, 100.0, 100,0)];
[myView.layer setContents:(id)[UIImage loadImageNamed:#"my.png"]];
3) Create CGPaths that describe the region in the UIView that you are interested in.
self.quadrantOnePath = CGPathCreateMutable();
CGPathMoveToPoint(self.quadrantOnePath, NULL, 50.0, 50.0);
CGPathAddLineToPoint(self.quadrantOnePath, NULL, 100.0, 50.0);
CGPathAddArc(self.quadrantOnePath, NULL, 50.0, 50.0, 50.0, 0.0, M_PI2, 1);
CGPathCloseSubpath(self.quadrantOnePath);
// create paths for the other 3 circle quadrants too!
4) Add a UIGestureRecognizer and listen/observe for taps in the view
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[tapRecognizer setNumberOfTapsRequired:2]; // default is 1
5) When tapRecognizer invokes its target selector
- (void)handleGesture:(UIGestureRecognizer *) recognizer {
CGPoint touchPoint = [recognizer locationOfTouch:0 inView:self.myView];
bool processTouch = CGPathContainsPoint(self.quadrantOnePath, NULL, touchPoint, true);
if(processTouch) {
// call your method to process the touch
}
}
Don't forget to release everything when appropriate -- use CGPathRelease to release paths.
Another thought: If the graphic that you are using to represent your circle quadrants is simply a filled color (i.e. no fancy graphics, layer effects, etc.), you could also use the paths you created in the UIView's drawRect method to draw the quadrants too. This would address one of the failings of the approach above: there isn't a tight integration between the graphic and the paths used to check for the touches. That is, if you swap out the graphic for something different, change the size of the graphic, etc., your paths used to check for touches will be out of sync. Potentially a high maintenance piece of code.
I can't see, why overlapping is needed.
Just create 4 buttons and give each one a slice of your image.
edit after comment
see this great project. One example is exactly what you want to do.
It works by incorporating the alpha-value of a pixel in the overwritten
pointInside:withEvent: and a category on UIImage, that adds this method
- (UIColor *)colorAtPixel:(CGPoint)point {
// Cancel if point is outside image coordinates
if (!CGRectContainsPoint(CGRectMake(0.0f, 0.0f, self.size.width, self.size.height), point)) {
return nil;
}
// Create a 1x1 pixel byte array and bitmap context to draw the pixel into.
// Reference: http://stackoverflow.com/questions/1042830/retrieving-a-pixel-alpha-value-for-a-uiimage
NSInteger pointX = trunc(point.x);
NSInteger pointY = trunc(point.y);
CGImageRef cgImage = self.CGImage;
NSUInteger width = self.size.width;
NSUInteger height = self.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
int bytesPerPixel = 4;
int bytesPerRow = bytesPerPixel * 1;
NSUInteger bitsPerComponent = 8;
unsigned char pixelData[4] = { 0, 0, 0, 0 };
CGContextRef context = CGBitmapContextCreate(pixelData,
1,
1,
bitsPerComponent,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(colorSpace);
CGContextSetBlendMode(context, kCGBlendModeCopy);
// Draw the pixel we are interested in onto the bitmap context
CGContextTranslateCTM(context, -pointX, pointY-(CGFloat)height);
CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, (CGFloat)width, (CGFloat)height), cgImage);
CGContextRelease(context);
// Convert color values [0..255] to floats [0.0..1.0]
CGFloat red = (CGFloat)pixelData[0] / 255.0f;
CGFloat green = (CGFloat)pixelData[1] / 255.0f;
CGFloat blue = (CGFloat)pixelData[2] / 255.0f;
CGFloat alpha = (CGFloat)pixelData[3] / 255.0f;
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}
Here's an awesome project that solves the problem of irregular shaped buttons so easily:
http://christinemorris.com/2011/06/ios-irregular-shaped-buttons/

Scaling MKMapView Annotations relative to the zoom level

The Problem
I'm trying to create a visual radius circle around a annonation, that remains at a fixed size in real terms. Eg. So If i set the radius to 100m, as you zoom out of the Map view the radius circle gets progressively smaller.
I've been able to achieve the scaling, however the radius rect/circle seems to "Jitter" away from the Pin Placemark as the user manipulates the view.
I'm lead to believe this is much easier to achieve on the forthcoming iPhone OS 4, however my application needs to support 3.0.
The Manifestation
Here is a video of the behaviour.
The Implementation
The annotations are added to the Mapview in the usual fashion, and i've used the delegate method on my UIViewController Subclass (MapViewController) to see when the region changes.
-(void)mapView:(MKMapView *)pMapView regionDidChangeAnimated:(BOOL)animated{
//Get the map view
MKCoordinateRegion region;
CGRect rect;
//Scale the annotations
for( id<MKAnnotation> annotation in [[self mapView] annotations] ){
if( [annotation isKindOfClass: [Location class]] && [annotation conformsToProtocol:#protocol(MKAnnotation)] ){
//Approximately 200 m radius
region.span.latitudeDelta = 0.002f;
region.span.longitudeDelta = 0.002f;
region.center = [annotation coordinate];
rect = [[self mapView] convertRegion:region toRectToView: self.mapView];
if( [[[self mapView] viewForAnnotation: annotation] respondsToSelector:#selector(setRadiusFrame:)] ){
[[[self mapView] viewForAnnotation: annotation] setRadiusFrame:rect];
}
}
}
The Annotation object (LocationAnnotationView)is a subclass of the MKAnnotationView and it's setRadiusFrame looks like this
-(void) setRadiusFrame:(CGRect) rect{
CGPoint centerPoint;
//Invert
centerPoint.x = (rect.size.width/2) * -1;
centerPoint.y = 0 + 55 + ((rect.size.height/2) * -1);
rect.origin = centerPoint;
[self.radiusView setFrame:rect];
}
And finally the radiusView object is a subclass of a UIView, that overrides the drawRect method to draw the translucent circles. setFrame is also over ridden in this UIView subclass, but it only serves to call [UIView setNeedsDisplay] in addition to [UIView setFrame:] to ensure that the view is redrawn after the frame has been updated.
The radiusView object's (CircleView) drawRect method looks like this
-(void) drawRect:(CGRect)rect{
//NSLog(#"[CircleView drawRect]");
[self setBackgroundColor:[UIColor clearColor]];
//Declarations
CGContextRef context;
CGMutablePathRef path;
//Assignments
context = UIGraphicsGetCurrentContext();
path = CGPathCreateMutable();
//Alter the rect so the circle isn't cliped
//Calculate the biggest size circle
if( rect.size.height > rect.size.width ){
rect.size.height = rect.size.width;
}
else if( rect.size.height < rect.size.width ){
rect.size.width = rect.size.height;
}
rect.size.height -= 4;
rect.size.width -= 4;
rect.origin.x += 2;
rect.origin.y += 2;
//Create paths
CGPathAddEllipseInRect(path, NULL, rect );
//Create colors
[[self areaColor] setFill];
CGContextAddPath( context, path);
CGContextFillPath( context );
[[self borderColor] setStroke];
CGContextSetLineWidth( context, 2.0f );
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextAddPath(context, path );
CGContextStrokePath( context );
CGPathRelease( path );
//CGContextRestoreGState( context );
}
Thanks for bearing with me, any help is appreciated.
Jonathan
First, what's foo used in the first function? And I'm assuming radiusView's parent is the annotation view, right?
The "Jittering"
Also, the center point of radiusView should coincide with that of the annotationView. This should solve your problem:
-(void) setRadiusFrame:(CGRect)rect{
rect.origin.x -= 0.5*(self.frame.size.width - rect.size.width);
rect.origin.y -= 0.5*(self.frame.size.height - rect.size.height);
[self.radiusView setFrame:rect]
}
Unnecessary method
You could set the frame directly on the radiusView and avoid the above calculation:
UIView * radiusView = [[[self mapView] viewForAnnotation: annotation] radiusView];
rect = [[self mapView] convertRegion:foo toRectToView: radiusView.superView];
[radiusView setFrame:rect];
When drawing the ellipse, don't use the rect passed to drawRect:, it doesn't have to be the same as the frame. It's safer to directly use self.frame
Unnecessary view
Now I gave the above points if you need to use the above hierarchy, but I don't see why don't you just draw your ellipses directly in the LocationAnnotationView? It's there for this purpose after all. This is how you do this:
when scaling, change the annotationView's rect directly:
rect = [[self mapView] convertRegion:foo toRectToView: self.mapView];
[[[self mapView] viewForAnnotation: annotation] setFrame:rect];
Move the implementation of drawRect: to LocationAnnotationView.
This is easier to implement, and should address your problem as the center point of the annotation view moves with your pin and you shouldn't see this problem.
Fixes
There are two other issues in the code:
Set longitudeDelta like this:
region.span.longitudeDelta = 0.002*cos(region.center.latitude*pi/180.0);
as the longitude delta converted to meters changes with the latitude. Alternatively, you could only set latitude delta, then modify the rect so it becomes rectangular (width==height).
in drawRect:, don't use the passed rect; instead use self.frame. There's no guarantee that these are the same, and rect could have any value.
Let me know if these work ;-)