13

I want to check if my UILabel was touched. But i need even more than that. Was the text touched? Right now I only get true/false if the UILabel frame was touched using this:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    if (CGRectContainsPoint([self.currentLetter frame], [touch locationInView:self.view]))
    {
        NSLog(@"HIT!");
    }
}

Is there any way to check this? As soon as I touch somewhere outside the letter in the UILabel I want false to get returned.

I want to know when the actual black rendered "text pixles" has been touched.

Thanks!

Joakim Serholt
  • 403
  • 5
  • 17
  • I could replace UILabel with any other object if needed, even Images would be ok, could PNG's with transparent background work somehow? – Joakim Serholt Jun 23 '13 at 10:48
  • What is `currentLetter`? What is "somewhere outside the letter"? – Wain Jun 23 '13 at 10:55
  • 1
    maybe make uibutton without background? – p.balmasov Jun 23 '13 at 11:09
  • 1
    Do you mean the space the text takes up with the label being bigger or the actual pixels being rendered black? – Christian Schnorr Jun 23 '13 at 11:43
  • @Jenox I need to check if the user touches the black rendered pixels. – Joakim Serholt Jun 23 '13 at 12:07
  • @ices_2 i have no problem switching from UILabel to UIButton, but please help me, how do i recognize that the actual rendered "textpixels" has been pressed. – Joakim Serholt Jun 23 '13 at 12:11
  • 1
    Don't you think the letters would be too thin for human fingers? – Khanh Nguyen Jun 23 '13 at 15:06
  • Agree with Nguyen, it's a stupid idea, but here's how you do it. Take a [snapshot](http://stackoverflow.com/questions/2200736/how-to-take-a-screenshot-programmatically) of the label and get the color value / rgb data of the touched point. – Christian Schnorr Jun 24 '13 at 11:05
  • Too small for human fingers? It depends on how big the characters are doesnt it? Im working with characters big enough for half ipad screen. Thanks for all replies in this thread ill work with those soon enough and get back with results. – Joakim Serholt Jun 28 '13 at 13:19
  • The real issue is recognizing pixels rather than the actual characters.... What I had to do with a c++ project last semester was image recognition. There may be a library iOS has to do such but your going to have to create a nested loop that actually searches through EVERY pixel... compare the background color to the character colors and if there is a color change, your going to have to store that pixel value "WITHIN THE UILABEL" inside of an object or arrar that will be retained throughout the lifetime of the app, or loaded view lifecycle... – jsetting32 Jun 28 '13 at 16:15
  • You can of course do this in c++ and I can provide the code I used for the image recognition but I used c++ like I said, with EasyBMP... You can just create an image in photoshop with the letter/text you would like to use and import the uilabel to have the background image as the newly created photoshop image. After that, create a class that runs the methods that processes the image and record the pixels, store them in an array object and refer to those pixels when adding the gesture recognizer... The task your attempting to do, in my opinion, is very advanced but is possible. – jsetting32 Jun 28 '13 at 16:20
  • @JoakimSerholt this is pretty cool since this can have some neat practical applications to it. I just don't know any. Could you please tell me what you used this for? And maybe some other examples applications where this concept can be used? Thank you man! – Pavan Feb 15 '14 at 00:57

8 Answers8

21

tl;dr: You can hit test the path of the text. Gist is available here.


The approach I would go with is to check if the tap point is inside the path of the text or not. Let me give you a overview of the steps before going into detail.

  1. Subclass UILabel
  2. Use Core Text to get the CGPath of the text
  3. Override pointInside:withEvent: to be able to determine if a point should be considered inside or not.
  4. Use any "normal" touch handling like for example a tap gesture recognizer to know when a hit was made.

The big advantage of this approach is that it follows the font precisely and that you can modify the path to grow the "hittable" area like seen below. Both the black and the orange parts are tappable but only the black parts will be drawn in the label.

tap area

Subclass UILabel

I created a subclass of UILabel called TextHitTestingLabel and added a private property for the text path.

@interface TextHitTestingLabel (/*Private stuff*/)
@property (assign) CGPathRef textPath;
@end

Since iOS labels can have either a text or an attributedText so I subclassed both these methods and made them call a method to update the text path.

- (void)setText:(NSString *)text {
    [super setText:text];

    [self textChanged];
}

- (void)setAttributedText:(NSAttributedString *)attributedText {
    [super setAttributedText:attributedText];

    [self textChanged];
}

Also, a label can be created from a NIB/Storyboard in which case the text will be set right away. In that case I check for the initial text in awake from nib.

- (void)awakeFromNib {
    [self textChanged];
}

Use Core Text to get the path of the text

Core Text is a low level framework that gives you full control over the text rendering. You have to add CoreText.framework to your project and import it to your file

#import <CoreText/CoreText.h>

The first thing I do inside textChanged is to get the text. Depending on if it's iOS 6 or earlier I also have to check the attributed text. A label will only have one of these.

// Get the text
NSAttributedString *attributedString = nil;
if ([self respondsToSelector:@selector(attributedText)]) { // Available in iOS 6
    attributedString = self.attributedText; 
}
if (!attributedString) { // Either earlier than iOS6 or the `text` property was set instead of `attributedText`
    attributedString = [[NSAttributedString alloc] initWithString:self.text
                                                       attributes:@{NSFontAttributeName: self.font}];
}

Next I create a new mutable path for all the letter glyphs.

// Create a mutable path for the paths of all the letters.
CGMutablePathRef letters = CGPathCreateMutable();

Core Text "magic"

Core Text works with lines of text and glyphs and glyph runs. For example, if I have the text: "Hello" with attributes like this " Hel lo " (spaces added for clarity). Then that is going to be one line of text with two glyph runs: one bold and one regular. The first glyph run contains 3 glyphs and the second run contains 2 glyphs.

I enumerate all the glyph runs and their glyphs and get the path with CTFontCreatePathForGlyph(). Each individual glyph path is then added to the mutable path.

// Create a line from the attributed string and get glyph runs from that line
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);

// A line with more then one font, style, size etc will have multiple fonts.
// "Hello" formatted as " *Hel* lo " (spaces added for clarity) is two glyph
// runs: one italics and one regular. The first run contains 3 glyphs and the
// second run contains 2 glyphs.
// Note that " He *ll* o " is 3 runs even though "He" and "o" have the same font.
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
    // Get the font for this glyph run.
    CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
    CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);

    // This glyph run contains one or more glyphs (letters etc.)
    for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
    {
        // Read the glyph itself and it position from the glyph run.
        CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
        CGGlyph glyph;
        CGPoint position;
        CTRunGetGlyphs(run, glyphRange, &glyph);
        CTRunGetPositions(run, glyphRange, &position);

        // Create a CGPath for the outline of the glyph
        CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
        // Translate it to its position.
        CGAffineTransform t = CGAffineTransformMakeTranslation(position.x, position.y);
        // Add the glyph to the 
        CGPathAddPath(letters, &t, letter);
        CGPathRelease(letter);
    }
}
CFRelease(line);

The core text coordinate system is upside down compared to the regular UIView coordinate system so I then flip the path to match what we see on screen.

// Transform the path to not be upside down
CGAffineTransform t = CGAffineTransformMakeScale(1, -1); // flip 1
CGSize pathSize = CGPathGetBoundingBox(letters).size; 
t = CGAffineTransformTranslate(t, 0, -pathSize.height); // move down

// Create the final path by applying the transform
CGPathRef finalPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);

// Clean up all the unused path
CGPathRelease(letters);

self.textPath = finalPath;

And now I have a complete CGPath for the text of the label.

Override pointInside:withEvent:

To customize what points the label consider as inside itself I override point inside and have it check if the point is inside the text path. Other parts of UIKit is going to call this method for hit testing.

// Override -pointInside:withEvent to determine that ourselves.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // Check if the points is inside the text path.
    return CGPathContainsPoint(self.textPath, NULL, point, NO);
}

Normal touch handling

Now everything is setup to work with normal touch handling. I added a tap recognizer to my label in a NIB and connected it to a method in my view controller.

- (IBAction)labelWasTouched:(UITapGestureRecognizer *)sender {
    NSLog(@"LABEL!");
}

That is all it takes. If you scrolled all the way down here and don't want to take the different pieces of code and paste them together I have the entire .m file in a Gist that you can download and use.

A note, most fonts are very, very thin compared to the precision of a touch (44px) and your users will most likely be very frustrated when the touches are considered "misses". That being said: happy coding!


Update:

To be slightly nicer to the user you can stroke the text path that you use for hit testing. This gives a larger area that hit tappable but still gives the feeling that you are tapping the text.

CGPathRef endPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);

CGMutablePathRef finalPath = CGPathCreateMutableCopy(endPath);
CGPathRef strokedPath = CGPathCreateCopyByStrokingPath(endPath, NULL, 7, kCGLineCapRound, kCGLineJoinRound, 0);
CGPathAddPath(finalPath, NULL, strokedPath);

// Clean up all the unused paths
CGPathRelease(strokedPath);
CGPathRelease(letters);
CGPathRelease(endPath);

self.textPath = finalPath;

Now the orange area in the image below is going to be tappable as well. This still feels like you are touching the text but is less annoying to the users of your app. tap area

If you want you can take this even further to make it even easier to hit the text but at some point it is going to feel like the entire label is tappable.

Huge tap area

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • I think this is the best technique for general hit-testing on text that you might render yourself. But for use with UILabel's it has some issues: 1) I couldn't get this to work until I resized the bounding box of my label to very closely contain the text. If the label frame was taller (or just default sizeToFit height), the hit testing didn't align with what was rendered in the y-axis. 2) doesn't account for any auto-truncation the UILabel might apply. 3) unsure about Autoshrink support - seems unlikely. 4) doesn't compile in Xcode 4.6 (does compile in a certain Developer Preview...) – TomSwift Jul 01 '13 at 23:28
  • The compilation error I'm seeing is due to an implicit bridge-cast for the parameter passed to CTLineCreateWithAttributedString. Adding __bridge to the cast makes it work. – TomSwift Jul 01 '13 at 23:31
  • @TomSwift that is some very good feedback. Thank you. I'll see what I can do to address those things. – David Rönnqvist Jul 02 '13 at 04:58
7

The problem, as I understand it, is to detect when a tap (touch) happens on one of the glyphs that comprise the text in a UILabel. If a touch lands outside the path of any of the glyphs then it isn't counted.

Here's my solution. It assumes a UILabel* ivar named _label, and a UITapGestureRecognizer associated with the view containing the label.

- (IBAction) onTouch: (UITapGestureRecognizer*) tgr
{
    CGPoint p = [tgr locationInView: _label];

    // in case the background of the label isn't transparent...
    UIColor* labelBackgroundColor = _label.backgroundColor;
    _label.backgroundColor = [UIColor clearColor];

    // get a UIImage of the label
    UIGraphicsBeginImageContext( _label.bounds.size );
    CGContextRef c = UIGraphicsGetCurrentContext();
    [_label.layer renderInContext: c];
    UIImage* i = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // restore the label's background...
    _label.backgroundColor = labelBackgroundColor;

    // draw the pixel we're interested in into a 1x1 bitmap
    unsigned char pixel = 0x00;
    c = CGBitmapContextCreate(&pixel,
                              1, 1, 8, 1, NULL,
                              kCGImageAlphaOnly);
    UIGraphicsPushContext(c);
    [i drawAtPoint: CGPointMake(-p.x, -p.y)];
    UIGraphicsPopContext();
    CGContextRelease(c);

    if ( pixel != 0 )
    {
        NSLog( @"touched text" );
    }
}
TomSwift
  • 39,369
  • 12
  • 121
  • 149
  • This worked just fine and took me 5 min to implement. Im quite sure there have been other good alternative suggestions in the thread, unfortinatly im not in the position where i can test all of them. Im giving you the answer @TomSwift, since your code seems to fit my needs. – Joakim Serholt Jul 01 '13 at 19:38
  • This is an awesome snippet of code, Tom! This concept of detecting a particular pixel's alpha channel to determine whether the user selected the glyphs or not is cool. Do you know of any practical applications for this? – Pavan Feb 15 '14 at 01:04
4

You can use a UIGestureRecognizer: http://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html

Specifically, I guess you'd like to use the UITapGestureRecognizer. If you want to recognize when the text frame is touched, then the easiest would be to make the size of your frame to fit the text with [yourLabel sizeToFit].

Anyway, to do so I will go to use a UIButton, it's the easiest option.

In case you need to detect only when the actual text and not the entire UITextField frame is tapped then it becomes much more difficult. One approach is detecting the darkness of the pixel the user tapped, but this involves some ugly code. Anyway, depending on the expected interaction within your application in can work out. Check this SO question:

iOS -- detect the color of a pixel?

I would take in consideration that not all the rendered pixel will be 100% black, so I would play with a threshold to achieve better results.

Community
  • 1
  • 1
atxe
  • 5,029
  • 2
  • 36
  • 50
  • You said it yourself, set frame size. I dont want to know when the user taps the frame, i want to know when the user taps the actual text, the black rendered pixels forming the letter. – Joakim Serholt Jun 23 '13 at 12:14
  • @JoakimBörjesson Answer updated ;-) Not an optimal solution (quite far of being so), but give it a try :-) – atxe Jun 25 '13 at 20:50
2

I think he wants to know whether the letter within the label is touched, not other parts of the label. Since you are willing to use a transparent image to achieve this, I would suggest that, for example you have the letter "A" with transparent background, if the color of the letter if monotonous, let's say red in this case, you could grab a CGImage of the UIImage, get the provider and render it as bitmap and sample whether the color of the point being touched is red. For other colors, you could simply sample that color using an online image editor and grab its RGB value and check against that.

Unheilig
  • 16,196
  • 193
  • 68
  • 98
  • Do you have an example of how i could check the color of the point being touched? Can't I validate that against the text color of the UILabel text aswell? Please elaborate a bit. – Joakim Serholt Jun 23 '13 at 12:39
0

You could use an UIButton instead of a label :

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    UIButton *tmpButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 100, 20)];
    [tmpButton setTitle:@"KABOYA" forState:UIControlStateNormal];
    [tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [tmpButton addTarget:self
              action:@selector(buttonPressed:)
    forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:tmpButton];
}

When the Button is pressed do something here :

-(void)buttonPressed:(UIButton *)sender {
    NSLog(@"Pressed !");
}

I hope it helped ;)

BaNz
  • 202
  • 2
  • 16
  • If i write the letter A for example. The uibutton touch will be triggered when i touch inside the frame (square) that is formed around the letter. What i need is to only trigger the event when the actual black pixels are touched. if you press inside an O then it should not trigger either. – Joakim Serholt Jun 23 '13 at 12:10
0

Assuming UILabel instance which you want to track is userInteractionEnabled.

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    UIView *touchView = touch.view;
    if([touchView isKindOfClass:[UILabel class]]){
        NSLog(@"Touch event occured in Label %@",touchView);
    }
}
IronMan
  • 1,505
  • 1
  • 12
  • 17
0

First of all create and attach tap gesture recognizer and allow user interactions:

UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
[self.label addGestureRecognizer:tapRecognizer];
self.label.userInteractionEnabled = YES;

Now implement -tapGesture:

- (void)tapGesture:(UITapGestureRecognizer *)recognizer
{
    // Determine point touched
    CGPoint point = [recognizer locationInView:self.label];

    // Render UILabel in new context
    UIGraphicsBeginImageContext(self.label.bounds.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.label.layer renderInContext:context];

    // Getting RGBA of concrete pixel
    int bpr = CGBitmapContextGetBytesPerRow(context);
    unsigned char * data = CGBitmapContextGetData(context);
    if (data != NULL)
    {
        int offset = bpr*round(point.y) + 4*round(point.x);
        int red = data[offset+0];
        int green = data[offset+1];
        int blue = data[offset+2];
        int alpha =  data[offset+3];

        NSLog(@"%d %d %d %d", alpha, red, green, blue);

        if (alpha == 0)
        {
            // Here is tap out of text
        }
        else
        {
            // Here is tap right into text
        }
    }

    UIGraphicsEndImageContext();
}

This will works on UILabel with transparent background, if this is not what you want you can compare alpha, red, green, blue with self.label.backgroundColor...

k06a
  • 17,755
  • 10
  • 70
  • 110
0

Create the Label in viewDidLoad or through IB and add tapGesture using below code with selector then when you tap on label log will be printed(which is in singletap:)

- (void)viewDidLoad
{
[super viewDidLoad];    
UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(30, 0, 150, 35)];
label.userInteractionEnabled = YES;
label.backgroundColor = [UIColor greenColor];
label.text = @"label";
label.textAlignment = NSTextAlignmentCenter;

UITapGestureRecognizer * single = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singletap:)];
[label addGestureRecognizer:single];
single.numberOfTapsRequired = 1;
[self.view addSubview:label];


}
-(void) singletap:(id)sender
{
NSLog(@"single tap");
//do your stuff here
}

If your found it please mark it positive happy coding

ashokdy
  • 1,001
  • 12
  • 21