I want to write text in a path that can take up any shape. Right now I can draw flipped text in it:
NSArray *coordinates = ...;
CGMutablePathRef pathRef = [self objectPathGivenCoordinates:coordinates];
... // Draw the shapes
// Now draw the text
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
NSAttributedString* attString = [[NSAttributedString alloc] initWithString:@"0,1,2..."];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attString length]), pathRef, NULL);
CTFrameDraw(frame, context);
That produces this:
I understand that iOS has a flipped coordinate system. Most solutions add these two lines before drawing:
CGContextTranslateCTM(context, 0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
But that produces this:
So why doesn't this technique work? In every example I've seen, this technique is used when text is rendered in something symmetric, or with clipping, or on OS X. If we're drawing text in a symmetric shape, our calls to CGContextTranslateCTM()
and CGContextScaleCTM()
make sense. A call to CGContextTranslateCTM(context, 0, rect.size.height)
, moves the origin of the first letter to the top. A call to CGContextScaleCTM(context, 1, -1)
reverses the direction of Y. If we reverse the direction of Y on a symmetric shape, it doesn't change. Sadly, my shapes aren't symmetric.
Here is what I've considered but abandoned
UIImageView
behind it somehow (1)CGContextSetTextMatrix()
so that text is drawn from left to right, bottom up, and is right side up. Determine how many extra characters are left in the shape after a string for the shape is determined. Reverse the words (now it will draw top to bottom, right to left, right side up). Add spaces for every extra character. Now for each CTLine
, somehow reverse the words again (now it will draw top to bottom, left to right, right side up) (2)Why I abandoned them
(1) I can't flip my images. They always need to be right side up
(2) This may be overkill for a simple solution someone else knows
I determined a solution
// Flip the coordinates so that the object is drawn as if it were mirrored along the x-axis
// coordinates = @[CGPointMake(1, 2), CGPointMake(9, 17), CGPointMake(41, 3), ...]
NSArray *flippedCoordinates = [coordinates map:^id(id object) {
CGPoint value;
[object getValue:&value];
CGPoint point = value;
point.y = point.y * (-1) + rect.size.height;
return [NSValue value:&point withObjCType:@encode(CGPoint)];
}];
// Generate a CGPathRef using the new coordinates
CGMutablePathRef pathRef = CGPathCreateMutable();
CGPathMoveToPoint(pathRef, NULL, [[flippedCoordinates objectAtIndex:0] CGPointValue].x + .5 [[flippedCoordinates objectAtIndex:0] CGPointValue].y + .5);
for(NSValue *arrayValue in flippedCoordinates) {
CGPoint point = [arrayValue CGPointValue];
CGPathAddLineToPoint(pathRef, NULL, point.x, point.y);
}
CGPathCloseSubpath(pathRef);
// Draw the object
// ...
// Draw the text as if it were on OS X
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
NSAttributedString* attString = [[NSAttributedString alloc] initWithString:@"0,1,2,..." attributes:@{(NSString *)kCTForegroundColorAttributeName : textColor}];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attString length]), pathRef, NULL);
CTFrameDraw(frame, context);
CFRelease(frame);
CFRelease(frameSetter);
CFRelease(pathRef);
// And, for the magic touch, flip the whole view AFTER everything is drawn
[self.layer setAffineTransform:CGAffineTransformMakeScale(1, -1)];
Now everything is drawn from top to bottom, left to right, right side up
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With