I have an NSMutableAttributedString such as "Bob liked your picture".
I'm wondering if I can add two different tap events to "Bob" and "picture". Ideally, tapping "Bob" would present a new view controller with Bob's profile and tapping "picture" would present a new view controller with the picture. Can I do this with NSMutableAttributedString?
You can achieve this by using CoreText to implement a method that will retrieve the index of the character the user selected / touched. First, using CoreText, draw your attributed string in a custom UIView sub class. An example overridden drawRect: method:
- (void) drawRect:(CGRect)rect
{
// Flip the coordinate system as CoreText's origin starts in the lower left corner
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0.0f, self.bounds.size.height);
CGContextScaleCTM(context, 1.0f, -1.0f);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.bounds];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(_attributedString));
if(textFrame != nil) {
CFRelease(textFrame);
}
// Keep the text frame around.
textFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path.CGPath, NULL);
CFRetain(textFrame);
CTFrameDraw(textFrame, context);
}
Secondly, create a method that interrogates the text to find the character index for a given point:
- (int) indexAtPoint:(CGPoint)point
{
// Flip the point because the coordinate system is flipped.
point = CGPointMake(point.x, CGRectGetMaxY(self.bounds) - point.y);
NSArray *lines = (__bridge NSArray *) (CTFrameGetLines(textFrame));
CGPoint origins[lines.count];
CTFrameGetLineOrigins(textFrame, CFRangeMake(0, lines.count), origins);
for(int i = 0; i < lines.count; i++) {
if(point.y > origins[i].y) {
CTLineRef line = (__bridge CTLineRef)([lines objectAtIndex:i]);
return CTLineGetStringIndexForPosition(line, point);
}
}
return 0;
}
Lastly, you can override the touchesBegan:withEvent: method to get the location of where the user touched and convert that into a character index or range:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
CGPoint tp = [t locationInView:self];
int index = [self indexAtPoint:tp];
NSLog(#"Character touched : %d", index);
}
Be sure to include CoreText into your project and clean up any resources (like text frames) you keep around as that memory is not managed by ARC.
The way I would handle it is using a standard NSString in a UITextView. Then taking advantage of the UITextInput protocol method firstRectForRange:. Then you could easily overlay an invisible UIButton in that rect and handle the action you'd like to take.
Related
I'm using core text API to draw text on UI. (I don't know other methods, because most Google search results lead me to core text api).
I have read some tutorials online about using core text API. In those tutorial, I see step by step to config from matrix, attribute string to framesetter ... but none of them explain carefully meaning of each step, so I cannot modify by myself.
Below code is the function draw text on screen with (x,y) is the location which I want to draw. This piece of code describe clearly step by step do draw on screen. Nevertheless I don't know where to put x and y parameters, so text will start to draw at this point in rectangle.
// draw text on screen.
+ (void)drawText:(CGContextRef)context bound:(CGRect)rect text:(NSString *)text x:(float)x y:(float) y color:(UIColor *)color size:(float)textSize{
CGContextSaveGState(context);
// always remember to reset the text matrix before drawing.
// otherwise the result will be unpredictable like using uninitialize memory
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1.0f, -1.0f));
// Flip the coordinate system. because core Text uses different coordinate system with UI Kit
CGContextTranslateCTM(context, 0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// step 1: prepare attribute string
NSDictionary *attributes;
attributes = #{
(NSString *) kCTFontAttributeName : [UIFont fontWithName:#"Helvetica" size:textSize],
(NSString *) kCTForegroundColorAttributeName : color
};
NSAttributedString *str = [[NSAttributedString alloc]
initWithString:text attributes:attributes];
CFAttributedStringRef attrString = (__bridge CFAttributedStringRef)str;
// step 2: create CTFFrameSetter
CTFramesetterRef framesetter =
CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
// step 3. create CGPath
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, rect);
// step 4. get the frame
// use the CGPath and CTFrameSetter to create CTFrame. then drawn it in currently context
CTFrameRef frame = CTFramesetterCreateFrame
(framesetter, CFRangeMake(0, 0), path, NULL);
CTFrameDraw(frame, context);
// step 5. release resource
//CFRelease(attrString);
CFRelease(framesetter);
CFRelease(frame);
CFRelease(path);
CGContextRestoreGState(context);
}
Moreover, I see there are function: CGContextSetTextPosition This seem what I need, but where should I put in above code. I have tried some, but no succeed. Please tell me how to fix this.
Thanks :)
This is not CoreText solution but after your comment under your question I assume this would be a proper answer.
Basic text drawing on a view.
.h
#import <UIKit/UIKit.h>
#interface aView : UIView
#end
.m
#import "aView.h"
#implementation aView
- (void)drawText:(CGFloat)xPosition yPosition:(CGFloat)yPosition canvasWidth:(CGFloat)canvasWidth canvasHeight:(CGFloat)canvasHeight
{
//Draw Text
CGRect textRect = CGRectMake(xPosition, yPosition, canvasWidth, canvasHeight);
NSMutableParagraphStyle* textStyle = NSMutableParagraphStyle.defaultParagraphStyle.mutableCopy;
textStyle.alignment = NSTextAlignmentLeft;
NSDictionary* textFontAttributes = #{NSFontAttributeName: [UIFont fontWithName: #"Helvetica" size: 12], NSForegroundColorAttributeName: UIColor.redColor, NSParagraphStyleAttributeName: textStyle};
[#"Hello, World!" drawInRect: textRect withAttributes: textFontAttributes];
}
- (void)drawRect:(CGRect)rect {
[self drawText:0 yPosition:0 canvasWidth:200 canvasHeight:150];
}
#end
Starting with IOS 7 you can have the str render itself into the current context. To do this import UIKit. This will extend the defenition of NSAttributedString with drawAtPoint: method. So for example you might do something like this:
[str drawAtPoint:CGPointMake(x,y)]
For more info about this and other related methods please see the following:
https://developer.apple.com/library/ios/documentation/uikit/reference/NSAttributedString_UIKit_Additions/Reference/Reference.html#//apple_ref/occ/instm/NSAttributedString/drawAtPoint:
On draw text selected view you can used UIView touch events as below.
create global variable for Touch start point and Touch end Point as below
CGPoint startingPoint;
CGPoint endingPoint;
And then draw text or line as below
#pragma mark- Touch Event
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
startingPoint = [touch locationInView:self];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
endingPoint = [touch locationInView:self];
[self makeLineLayer:self.layer lineFromPointA:startingPoint
toPointB:endingPoint];
}
-(void)makeLineLayer:(CALayer *)layer lineFromPointA:(CGPoint)pointA
toPointB:(CGPoint)pointB
{
CAShapeLayer *line = [CAShapeLayer layer];
UIBezierPath *linePath=[UIBezierPath bezierPath];
[linePath moveToPoint: pointA];
[linePath addLineToPoint:pointB];
line.path=linePath.CGPath;
line.fillColor = nil;
line.opacity = 2.0;
line.lineWidth = 4.0;
line.strokeColor = [UIColor redColor].CGColor;
[layer addSublayer:line];
}
Although this thread is a bit old, but this is my work around for future visitors. This work requires a little bit modification and it starts moving the text to position (x, y).
You have to change step 3 like this:
// step 3. create CGPath
CGMutablePathRef path = CGPathCreateMutable();
CGRect newRect = rect;
newRect.origin.x = x;
newRect.origin.y = -y;
CGPathAddRect(path, NULL, newRect);
and the coordinate (x, y) will start working. The minus sign is due to the different axis of CoreTextAPI.
Also changed the TextMatrix to below for getting the correct transformation or it was giving a flipped Y-Axis text:
// always remember to reset the text matrix before drawing.
// otherwise the result will be unpredictable like using uninitialize memory
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1.0f, 1.0f));
Attached screenshot from IB (Designable):
Hope it helps!
i can draw points on my image using the code in touchesbegan. and i am storing my coordinates in NSMutable array. i want it to draw lines as i mark points on the screen.. but my drawRect isnt firing i guess .. can u please tell me what to do..
-(void) drawRect:(CGRect)rect
{
int *count = 0;
if([pointarray count]!=0)
{
float firstpointx= [[pointarray objectAtIndex:0]floatValue];
float firstpointy= [[pointarray objectAtIndex:1]floatValue];
float secondpointx= [[pointarray objectAtIndex:2]floatValue];
float secondpointy= [[pointarray objectAtIndex:3]floatValue];
//NSMutableArray *coordinates = [[NSMutableArray alloc] init];
for (int i=0; i<=[pointarray count]; i++) {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 2.0);
CGContextMoveToPoint(ctx, firstpointx, firstpointy);///move to ur first dot
CGContextAddLineToPoint(ctx, secondpointx, secondpointy);//add line from first dot to second dot
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextStrokePath(ctx);
[pointarray removeAllObjects];//remove first two points from ur array so that next line is not drawn in continuous with previous line
}
count++;
}
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
pointarray=[[NSMutableArray alloc]init];
CGPoint curPoint = [[touches anyObject] locationInView:self.map];
[pointarray addObject:[NSNumber numberWithFloat:curPoint.x]];
[pointarray addObject:[NSNumber numberWithFloat:curPoint.y]];
[_map setNeedsDisplay];
[self logMessage:[NSString stringWithFormat:#"Sending : %#", pointarray]];
NSLog(#"the point array is %#",pointarray);
NSArray *coordinates = [[NSArray alloc]initWithArray:pointarray copyItems:YES];
NSLog(#"the coordinate array %#",coordinates);
//[self.map setNeedsDisplay]; // calls drawRectMethod
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(curPoint.x, curPoint.y, 10, 10)];
view.backgroundColor = [UIColor redColor];
[self.map addSubview:view];
}
On touchedBegan:withEvent: you just call setNeedsDisplay on _map, not on self, that's why the view doesn't get redraw.
Also, you just add two points, but in drawRect you code like if it's sure that the array contains four points. Probably what you want to do is to add two points in touchesEnded:withEvent? If so you should call setNeedsDisplay from there.
You should call setNeedsDisplay from this method:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
I have tried and searched a lot but have not find a solutions.
I have f.e this picture as a .png [1]: http://imm.io/1f3DY
If the user touches f.e. in the area 1
I want to fill up this area with color f.e. blue.
Is there any way to find out/create these areas in code and fill it up with colors?
I have convert my .png to .svg and created some bezierpathes. Stored the bezierpathes in an array and add a gesturerecognizer. This works fine. Got the right path which I have touched. Can change the color of the path. But only the black lines. I want to fill up the space between f.e. path1 and path 2. How can I do this?
//The event handling method
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
CGPoint location = [recognizer locationInView:[recognizer.view superview]];
location.y = self.view.bounds.size.height - location.y;
for(int i = 0; i< bezierPathes.count; i++)
{
//if([b containsPoint:location])
UIBezierPath* b = bezierPathes[i];
if([[self tapTargetForPath:b] containsPoint:location])
{
[[UIColor redColor] setFill];
[b fill];
[testView setNeedsDisplay];
}
}
}
// this method will let you easily select a bezier path ( 15 px up and down of a path drawing)
- (UIBezierPath *)tapTargetForPath:(UIBezierPath *)path
{
if (path == nil) {
return nil;
}
CGPathRef tapTargetPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, fmaxf(35.0f, path.lineWidth), path.lineCapStyle, path.lineJoinStyle, path.miterLimit);
if (tapTargetPath == NULL) {
return nil;
}
UIBezierPath *tapTarget = [UIBezierPath bezierPathWithCGPath:tapTargetPath];
CGPathRelease(tapTargetPath);
return tapTarget;
}
You can also do this with Flood Fill algorithm.
Hear is the link http://en.wikipedia.org/wiki/Flood_fill
I have a project about drawing,I will use my finger to draw something.When my finger move faster the size of brush will be more bigger.How can I code it???
This is the cods about the drawect:
In my project I use UItouch to check my finger.
(void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event
{
if (drawEnable==YES) {
NSArray * touchesArr=[[event allTouches] allObjects];
if ([touchesArr count]==1) {
NSMutableArray *arrayPointsInStroke = [NSMutableArray array];
NSMutableDictionary *dictStroke = [NSMutableDictionary dictionary];
[dictStroke setObject:arrayPointsInStroke forKey:#"points"];
[dictStroke setObject:self.currentColor forKey:#"color"];
[dictStroke setObject:[NSNumber numberWithFloat:self.currentSize] forKey:#"size"];
CGPoint point = [[touches anyObject] locationInView:self];
[arrayPointsInStroke addObject:NSStringFromCGPoint(point)];
[self.arrayStrokes addObject:dictStroke];
lastDistance=0;
}
}
}
// Add each point to points array
- (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event
{
float sub_x,sub_y;
float currentDistance;
if (penStyle==1) {
CGPoint point = [[touches anyObject] locationInView:self];
CGPoint prevPoint = [[touches anyObject] previousLocationInView:self];
NSMutableArray *arrayPointsInStroke = [[self.arrayStrokes lastObject] objectForKey:#"points"];
[arrayPointsInStroke addObject:NSStringFromCGPoint(point)];
CGRect rectToRedraw = CGRectMake(\
((prevPoint.x>point.x)?point.x:prevPoint.x)-currentSize,\
((prevPoint.y>point.y)?point.y:prevPoint.y)-currentSize,\
fabs(point.x-prevPoint.x)+2*currentSize,\
fabs(point.y-prevPoint.y)+2*currentSize\
);
[self setNeedsDisplayInRect:rectToRedraw];
}
}
}
// Send over new trace when the touch ends
- (void) touchesEnded:(NSSet *) touches withEvent:(UIEvent *) event
{
[self.arrayAbandonedStrokes removeAllObjects];
}
// Draw all points, foreign and domestic, to the screen
- (void) drawRect: (CGRect) rect
{
//int width = self.pickedImage.size.width;
//int height = self.pickedImage.size.height-44;
//CGRect rectForImage = CGRectMake(512-width/2, 384-height/2, width, height);
//[self.pickedImage drawInRect:rectForImage];
if (self.arrayStrokes)
{
int arraynum = 0;
// each iteration draw a stroke
// line segments within a single stroke (path) has the same color and line width
for (NSDictionary *dictStroke in self.arrayStrokes)
{
NSArray *arrayPointsInstroke = [dictStroke objectForKey:#"points"];
UIColor *color = [dictStroke objectForKey:#"color"];
float size = [[dictStroke objectForKey:#"size"] floatValue];
[color set]; // equivalent to both setFill and setStroke
// draw the stroke, line by line, with rounded joints
UIBezierPath* pathLines = [UIBezierPath bezierPath];
CGPoint pointStart = CGPointFromString([arrayPointsInstroke objectAtIndex:0]);
[pathLines moveToPoint:pointStart];
for (int i = 0; i < (arrayPointsInstroke.count - 1); i++)
{
CGPoint pointNext = CGPointFromString([arrayPointsInstroke objectAtIndex:i+1]);
[pathLines addLineToPoint:pointNext];
}
pathLines.lineWidth = size;
pathLines.lineJoinStyle = kCGLineJoinRound;
pathLines.lineCapStyle = kCGLineCapRound;
[pathLines stroke];
arraynum++;
}
}
}
Take a look at Smooth Drawing, which you can find here:
https://github.com/krzysztofzablocki/smooth-drawing
it does exactly what you need, by using UIPanGestureRecognizer's velocityInView method, and storing its values in an Array for using when drawing.
I have used "Register yourself" as text. I want to make it as link. And by clicking on that link, it should open Register.xib.
How can I get this link??
Kareem is right. Create a UIButton object, set it's type to "Custom" and then you can give it a title. Connect the action to a method which pushes or presents your Register view controller and you will be all set.
You'll probably want to give some indication to the user that the text is a clickable link. Either set the color of the text to blue. Another, completely separate option, is to use a NSAttributedString (which has an underline attribute) to indicate to the user that the text is clickable. That requires an Open Source replacement for UITextView (which you can learn more about from this related question).
I think by below you will get what you want...
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSSet *allTouches = [event allTouches];
if ([allTouches count] == 1)
{
UITouch *touch = (UITouch *)[allTouches anyObject];
CGPoint touchPoint = [touch locationInView:self];
// Convert to coordinate system of current view
touchPoint.y -= self.bounds.size.height;
touchPoint.y *= -1;
CGPathRef statusPath = CTFrameGetPath(statusCTFrame);
NSString *touchedInterStr = nil;
if (CGPathContainsPoint(statusPath, NULL, touchPoint, FALSE))
{
touchedInterStr = [self getInteractiveStringInFrame:statusCTFrameatPosition:touchPoint];
}
else
{
return ;
}
}
}
There's a middle ground that's worth mentioning. Using UIButton with UIButtonTypeCustom has some shortcomings, namely that you have limited control over the text styling (e.g. you cannot make it right or left aligned, it will always be centered).
Using NSAttributedString can be overkill, and tough to implement if you're unfamiliar with NSAttributedString, CoreText etc.
A solution I've used is a simple subclass of UIControl which allows more customization than subclassing UIButton and less hassle than NSAttributedString et al. The UIControl approach is only appropriate if the entire string will be a "link".
You would do something like this in your UIControl subclass:
Add a UILabel subview in the init method
Configure the UILabel with your desired backgroundColor, textColor, textAlignment etc, including changes to the UILabel layer if you want the background to appear like Twitter's in-tweet links in their iOS App
Override the setHighlighted: method to customize the look and feel changes on touch/tracking
Add #property setters in the .h file so the owning class can set the state
First, construct the attributed string and save the range of the interactive text and the text itself.
Then, draw the attributed string with CoreText Framework, keep the CTFrameRef.
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, bounds);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFRelease(framesetter);
CTFrameDraw(frame, context);
//CFRelease(frame);
CGPathRelease(path);
The last, override the [view touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event] like this:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSSet *allTouches = [event allTouches];
if ([allTouches count] == 1) {
UITouch *touch = (UITouch *)[allTouches anyObject];
CGPoint touchPoint = [touch locationInView:self];
// Convert to coordinate system of current view
touchPoint.y -= self.bounds.size.height;
touchPoint.y *= -1;
CGPathRef statusPath = CTFrameGetPath(statusCTFrame);
NSString *touchedInterStr = nil;
if (CGPathContainsPoint(statusPath, NULL, touchPoint, FALSE)) {
touchedInterStr = [self getInteractiveStringInFrame:statusCTFrame atPosition:touchPoint];
} else {
return ;
}
}
}
- (NSString *)getInteractiveStringInFrame:(CTFrameRef)frame atPosition:(CGPoint)point
{
CGPathRef path = CTFrameGetPath(frame);
CGRect rect;
CGPathIsRect(path, &rect);
// Convert point into rect of current frame, to accurately perform hit testing on CTLine
CGPoint pointInsideRect = point;
pointInsideRect.x = point.x - rect.origin.x;
CFArrayRef lines = CTFrameGetLines(statusCTFrame);
CFIndex count = CFArrayGetCount(lines);
CGFloat curUpBound = rect.origin.y + rect.size.height;
CFIndex touchedIndex = -1;
for (CFIndex pos = 0; pos < count; pos++) {
CTLineRef curLine = CFArrayGetValueAtIndex(lines, pos);
CGFloat ascent, descent, leading;
CTLineGetTypographicBounds(curLine, &ascent, &descent, &leading);
CGFloat curHeight = ascent + descent + leading;
if (pointInsideRect.y >= curUpBound-curHeight && pointInsideRect.y <= curUpBound){
touchedIndex = CTLineGetStringIndexForPosition(curLine, pointInsideRect); // Hit testing
break;
}
curUpBound -= curHeight;
}
if (touchedIndex == -1)
return nil;
NSEnumerator *enumerator = [interactiveStrs objectEnumerator];
InteractiveString *curString;
while ((curString = (InteractiveString *)[enumerator nextObject])) {
if (NSLocationInRange(touchedIndex, curString.range)) {
return curString.content;
}
}
return nil;
}
You get the interactive text in touchesEnded, then you could do anything you want, like trigger a delegate method.
PS: this is the old-fashion solution, I heard that iOS5 had provided something like an enhanced UIWebView to implement the requirements, maybe you could check the apple sdk document library.
Take a button and set title of the button which you want to set as hyperlink Text. You can choose default and custom button type button in iOS 7 and iOS 8, but for below iOS 7 you have to take button type custom only. You can change colour of the text to highlight text for UI and than add whatever action you want to add on that button click. It will work like charm, good luck.
Take a look to TTTAttributedLabel. Is really simple to configure links, phones, etc.