Hey i need a help with building an app. I build an art app, this app is working with interferencies. Thats the reason i need to draw many lines in this app. More lines are better for the interefernces. I think the problem is the iPad can't handle too many lines, because the speed or the performance is too slow.
I don't know how can i speed up my code for more performance on the iPad. Should i use Open GL or something else...
What can i do?
Here are the Draw.m
#import "Draw.h"
#implementation Draw
- (IBAction) sliderValueChanged:(UISlider *)sender {
label.text = [NSString stringWithFormat:#"%f", slider.value];
//NSLog(#"slider value = %f", sender.value);
[self setNeedsDisplay];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
//NSLog(#"slider value = %f", self.bounds.size.width);
CGMutablePathRef cgpath = CGPathCreateMutable();
CGPathMoveToPoint(cgpath, NULL, 0, 500);
CGMutablePathRef cgpath2 = CGPathCreateMutable();
CGPathMoveToPoint(cgpath2, NULL, 0, 500);
UIBezierPath *uipath = [[UIBezierPath alloc] init];
[uipath moveToPoint:CGPointMake(0, 0)];
int step = 5;
int iterations = self.bounds.size.width/step;
for (int i = 0; i < iterations+1; i++){
//CGPathAddCurveToPoint(cgpath, NULL, 1+i, 0, 1+i, 0, 1+i ,0);
CGPathAddLineToPoint ( cgpath, NULL, 0, 0 );
CGPathAddLineToPoint ( cgpath, NULL, 0, 768 );
CGPathAddLineToPoint ( cgpath, NULL, step*i-slider.value*2, 768 );
CGPathAddLineToPoint ( cgpath, NULL, step*i, 0 );
CGPathAddLineToPoint ( cgpath, NULL, (step*i)+step, 0 );
[[UIColor blackColor] setStroke];
CGContextAddPath(ctx, cgpath);
[self strokeUIBezierPath:uipath];
CGPathRelease(cgpath);
}
- (void)strokeContext:(CGContextRef)context
{
CGContextStrokePath(context);
}
- (void)strokeUIBezierPath:(UIBezierPath*)path
{
[path stroke];
}
#end
image http://img17.imageshack.us/img17/375/53178410200339197475308.jpg
the problem with bezier paths is, that they can be quite 'calculation heavy'.
You could either use straight lines ( CGContextAddLineToPoint(context, point.x, point.y);)
Or you use graphics acceleration.
You could either dive directly into OpenGL or use a game engine to help you with some of the code.
One of the most popular ones ( and as I think quite easy to use ) is cocos2d.
You should be getting better performance using a pattern to fill the screen. There is an entire section with sample code in the Quartz Programming Guide.
In your case you could create a very small pattern cell (height = 1) with just one black pixel to the very left followed by the same number of white pixels as the distance to the next line.
Related
I am using the following core graphics code to draw a waveform of audio data as I am recording. I apologize for tons of code, but here it is (I found it here):
//
// WaveformView.m
//
// Created by Edward Majcher on 7/17/14.
//
#import "WaveformView.h"
//Gain applied to incoming samples
static CGFloat kGain = 10.;
//Number of samples displayed
static int kMaxWaveforms = 80.;
#interface WaveformView ()
#property (nonatomic) BOOL addToBuffer;
//Holds kMaxWaveforms number of incoming samples,
//80 is based on half the width of iPhone, adding a 1 pixel line between samples
#property (strong, nonatomic) NSMutableArray* bufferArray;
+ (float)RMS:(float *)buffer length:(int)bufferSize;
#end
#implementation WaveformView
- (void)awakeFromNib
{
[super awakeFromNib];
self.bufferArray = [NSMutableArray array];
}
-(void)updateBuffer:(float *)buffer withBufferSize:(UInt32)bufferSize
{
if (!self.addToBuffer) {
self.addToBuffer = YES;
return;
} else {
self.addToBuffer = NO;
}
float rms = [WaveformView RMS:buffer length:bufferSize];
if ([self.bufferArray count] == kMaxWaveforms) {
//##################################################
// [self.bufferArray removeObjectAtIndex:0];
}
[self.bufferArray addObject:#(rms * kGain)];
[self setNeedsDisplay];
}
+ (float)RMS:(float *)buffer length:(int)bufferSize {
float sum = 0.0;
for(int i = 0; i < bufferSize; i++) {
sum += buffer[i] * buffer[i];
}
return sqrtf( sum / bufferSize );
}
// *****************************************************
- (void)drawRect:(CGRect)rect
{
CGFloat midX = CGRectGetMidX(rect);
CGFloat maxX = CGRectGetMaxX(rect);
CGFloat midY = CGRectGetMidY(rect);
CGContextRef context = UIGraphicsGetCurrentContext();
// Draw out center line
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, 1.);
CGContextMoveToPoint(context, 0., midY);
CGContextAddLineToPoint(context, maxX, midY);
CGContextStrokePath(context);
CGFloat x = 0.;
for (NSNumber* n in self.bufferArray) {
CGFloat height = 20 * [n floatValue];
CGContextMoveToPoint(context, x, midY - height);
CGContextAddLineToPoint(context, x, midY + height);
CGContextStrokePath(context);
x += 2;
}
if ([self.bufferArray count] >= kMaxWaveforms) {
[self addMarkerInContext:context forX:midX forRect:rect];
} else {
[self addMarkerInContext:context forX:x forRect:rect];
}
}
- (void)addMarkerInContext:(CGContextRef)context forX:(CGFloat)x forRect:(CGRect)rect
{
CGFloat maxY = CGRectGetMaxY(rect);
CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(x - 1.5, 0, 3, 3));
CGContextMoveToPoint(context, x, 0 + 3);
CGContextAddLineToPoint(context, x, maxY - 3);
CGContextStrokePath(context);
CGContextFillEllipseInRect(context, CGRectMake(x - 1.5, maxY - 3, 3, 3));
}
#end
So as I am recording audio, the waveform drawing gets more and more jittery, kind of like a game that has bad frame rates. I tried contacting the owner of this piece of code, but no luck. I have never used core graphics, so I'm trying to figure out why performance is so bad. Performance starts to degrade at around 2-3 seconds worth of audio (the waveform doesn't even fill the screen).
My first question is, is this redrawing the entire audio history every time drawRect is called? If you look in the drawRect function (marked by asterisks), there is a variable called CGRect x. This seems to affect the position at which the waveform is being drawn (if you set it to 60 instead of 0, it starts at x=60 pixels instead of x=0 pixels).
From my viewController, I pass in the audioData which gets stored in the self.bufferArray property. So when that loop goes through to draw the data, it seems like it's starting at zero and working its way up every time drawRect is getting called, which means that for every new piece of audio data added, drawRect gets called, and it redraws the entire waveform plus the new piece of audio data.
If that is the problem, does anyone know how I can optimize this piece of code? I tried emptying the bufferArray after the loop so that it contained only new data, but that didn't work.
If this is not the problem, are there any core graphics experts that can figure out what the problem is?
I should also mention that I commented out a piece of code (marked with ### at signs) because I need the entire waveform. I don't want it to remove pieces of the waveform at the beginning. The iOS Voice Memos app can hold a waveform of audio without performance degradation.
I am new to Core Graphics and trying to understand why text labels I draw in CGRect form an elliptical arc when images I draw using the same coordinates form a circular arc.
The original code by Arthur Knopper creates circles wherever the screen is touched. By removing the touches method, I have been able to generate a series of small circles (dots) along a circular arc (uber circle). Each dot is centred on the perimeter of the uber circle (as shown below).
In order to label each dot I use the same point coordinates I used for placing the dot. However text labels form an elliptical arc even though dots form a circular arc (as shown below). Labels are also hidden by the dots when dots are filled. The reason for this is a complete mystery.
As a novice I am probably missing something basic in Core Graphics. If anyone could explain what that is and what I need to do to make both arcs circular and place labels on top of the dots I’d be most grateful.
Thanks.
Here is the code.
circleView.h
NSMutableArray *totalCircles;
int dotCount, limit;
float uberX, uberY, uberRadius, uberAngle, labelX,
labelY,dotRadius, dotsFilled, sectors, x, y;
CGPoint dotPosition;
CGRect boxBoundary;
CGContextRef context;
}
- (void)demo;
#end
And ...
-#implementation iOSCircleView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
totalCircles = [[NSMutableArray alloc] init];
// Set background color
self.backgroundColor = [UIColor whiteColor];
}
return self;
} // frame a view for drawing
- (void)drawRect:(CGRect)rect {
[self demo];
}
- (void)demo {
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 120;
uberY = 160;
uberRadius = 30;
sectors = 16;
uberAngle = (2.0 * PI) / sectors;
dotRadius = 20;
dotsFilled = FALSE;
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // make new point for each dot
dotPosition = CGPointMake(x,y); // create each dot
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
[self autoLabel]; // text hides behind the dots
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES); // draw the circles
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (dotsFilled) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
//CGContextDrawPath(context, kCGPathFillStroke);
break;
default:
//CGContextStrokePath(context); // draw dot outline
break;
}
dotCount++;
}
} // draw circular dots in circular patterns
- (void)setSectorDotCoordinates {
x = uberX + (uberRadius * cos(uberAngle *dotCount) * 2);
y = uberY + (uberRadius * sin(uberAngle *dotCount) * 2);
} // calculate dot coordinates along a circular arc
- (void)autoLabel {
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius, x+dotRadius, y+dotRadius);
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary withFont:[UIFont systemFontOfSize:24] lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
Change the boxBoundary in autoLabel, CGRectMake creates a rectangle with one point coordinates and width and height, not two points:
(void)autoLabel {
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius, dotRadius*2, dotRadius*2);
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary
withFont:[UIFont systemFontOfSize:24]
lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
In your code the "boxes" containing the texts where bigger and bigger when you where going to the right. (the width and height were not fixed)
My updated code show labels that match the drawing order but text is still hidden when dots are filled.
I suspect I need to construct a path to write text in front of the dots and it’s already apparent that something like CGPathMoveToPoint is needed to start drawing from the 12 O'clock position.
Here’s the updated code. The first part draws and renders the dots
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 160;
uberY = 240;
uberRadius = 52;
sectors = 16;
uberAngle = ((2.0 * PI) / sectors);
NSLog(#"%f %f %f %f", uberX, uberY, uberRadius, uberAngle);
dotRadius = 20;
dotsFilled = FALSE;
textOffset = 4; // add to y to centre the label
for (dotCount = 1; dotCount <= 4 /*sectors*/; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // create a new point for each dot
dotPosition = CGPointMake(x,y); // create each dot
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES);
// draw the circles
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (dotsFilled) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
break;
default:
CGContextStrokePath(context); // draw dot outline
break;
}
[self setSectorDotCoordinates]; // find point coordinates for each dot
dotCount++;
}
The code that draws the labels follow immediately afterwards.
// draw labels
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // find point coordinates for each dot
dotPosition = CGPointMake(x,y); // use point coordinates for label
[self autoLabel]; // THIS SHOWS TEXT BEHIND THE DOTS
}
I'm trying to create a UIView in UIScrollView that contains just a simple grid (lines as rows and columns) drown by UIBezierPath or using the CG functions. The problem is, that when I have larger content size of the UIScrollView (as well as the larger subview), during the drawing of the grid huge amount of memory is allocated (50MB or more).
UIViewController which includes just UIScrollView over whole scene - adding subview in viewDidLoad:
#interface TTTTestViewController()
#property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
#end
#implementation TTTTestViewController
-(void)viewDidLoad
{
[super viewDidLoad];
// create the subview
TTTTestView *testView = [[TTTTestView alloc] init];
[self.scrollView addSubview:testView];
//set its properties
testView.cellSize = 50;
testView.size = 40;
// set the content size and frame of testView by the properties
self.scrollView.contentSize = CGSizeMake(testView.cellSize * testView.size, testView.cellSize * testView.size);
testView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height);
// let it draw the grid
[testView setNeedsDisplay];
}
#end
Inner view that just draw the grid using UIBezierPath/CG functions - depends on properties size(rows/columns count) and cellSize (width/height of one cell in grid):
#define GRID_STROKE_WIDTH 2.0
#implementation TTTTestView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[self drawGrid];
}
-(void)drawGrid
{
UIBezierPath *path = [[UIBezierPath alloc] init];
for (int i = 1; i < self.size; i++) {
//draw row line
[path moveToPoint:CGPointMake(0, self.cellSize * i)];
[path addLineToPoint:CGPointMake(self.bounds.size.width, self.cellSize * i)];
// draw column line
[path moveToPoint:CGPointMake(self.cellSize * i, 0)];
[path addLineToPoint:CGPointMake(self.cellSize * i , self.bounds.size.height)];
}
[path setLineWidth:GRID_STROKE_WIDTH];
[[UIColor blackColor] setStroke];
[path stroke];
/*
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, GRID_STROKE_WIDTH);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
for (int i = 1; i < self.size; i++) {
//draw row line
CGContextMoveToPoint(context, 0, self.cellSize * i );
CGContextAddLineToPoint(context, self.bounds.size.width, self.cellSize * i);
// draw column line
CGContextMoveToPoint(context, self.cellSize * i , 0);
CGContextAddLineToPoint(context, self.cellSize * i , self.bounds.size.height);
}
CGContextStrokePath(context);
*/
}
#end
Example1: self.size is 10, self.cellSize is 200 => contentSize is 2000x2000 points as well as frame of inner view => 18 lines are drown and it allocates ~60MB memory
Example2: self.size is 30, self.cellSize is 70 => contentSize is 2100x2100 points as well as frame of inner view => 58 lines are drown and it allocates ~67MB memory
These memory numbers I can see when debug the drawing method. No matter how I draw the lines, huge amount of memory is allocated when calling [path stroke] resp. CGContextStrokePath(context). In instruments I can see the biggest memory allocation at line:
12658 0x10200000 VM: CoreAnimation 00:04.092.149 • 67,29 MB QuartzCore CA::Render::Shmem::new_shmem(unsigned long)
I'm quite new in iOS programming and I was searching the solution everywhere and I still have no idea :-/ Can anyone please help me find some explanation what is going on here? Thanks :)
After asking on apple developer forum, I find out, that this is properly allocated memory in fact. It's because any view that uses -drawRect: to draw will use memory on the order of (bounds.size.width * bounds.size.height * contentScale * contentScale * 4) bytes.
The simplest way to create a grid that avoids that is to use add a view for each line and use the view's backgroundColor property to color the view. This will use hardly any memory because the view's (which can be plain UIViews) don't need to call -drawRect:, and thus won't use extra memory to store the results of your drawing.
I am looking to animate a single-line bezier curve on a loop in iOS. The idea I have in my head resembles the Voice Control screen on the iPhone 4 before Siri. The curve does not need to react to anything ie. Audio, mic etc. It just needs to loop from screen left to screen right, and change the amplitude of the curve.
I have tried a couple tests and this is the closest I have come:
IOS : Animate transformation from a line to a bezier curve
I need to know how to animated the actual curve to appear as if it is moving, not just up and down.
If any one has some light to shed on this, that would be awesome!
Thanks!
Wow, I worked on the exact same thing today. :)
Check this :
So the view where I draw my waves, is initialized as :
_self_view = [[TDTWaveView alloc] initWithFrame:CGRectMake(-320, 174, 640, 200)];
Then in my viewDidLoad, I call [self animateWave]; once.
- (void)animateWave {
[UIView animateWithDuration:.5 delay:0.0 options:UIViewAnimationOptionRepeat|UIViewAnimationOptionCurveLinear animations:^{
_self_view.transform = CGAffineTransformMakeTranslation(+_self_view.frame.size.width/2, 0);
} completion:^(BOOL finished) {
_self_view.transform = CGAffineTransformMakeTranslation(0, 0);
}];
}
This gives the wave a sort of linear motion you might want.
As far as the code for the wave goes, I'll share the drawrect.
self.yc = 30//The height of a crest.
float w = 0;//starting x value.
float y = rect.size.height;
float width = rect.size.width;
int cycles = 7;//number of waves
self.x = width/cycles;
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGContextSetLineWidth(context, .5);
while (w <= width) {
CGPathMoveToPoint(path, NULL, w,y/2);
CGPathAddQuadCurveToPoint(path, NULL, w+self.x/4, y/2 - self.yc, w+self.x/2, y/2);
CGPathAddQuadCurveToPoint(path, NULL, w+3*self.x/4, y/2 + self.yc, w+self.x, y/2);
w+=self.x;
}
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathStroke);
I have problem related with question
I've set contentsScale and after that text looking good but if I apply the 3d rotation transformation the text comes out blurry.
image here
initialization code
// init text
textLayer_ = [CATextLayer layer];
…
textLayer_.contentsScale = [[UIScreen mainScreen] scale];
// init body path
pathLayer_ = [CAShapeLayer layer];
…
[pathLayer_ addSublayer:textLayer_];
rotation code
// make the mirror
pathLayer_.transform = CATransform3DRotate(pathLayer_.transform, M_PI, 0, 1, 0);
textLayer_.transform = CATransform3DRotate(textLayer_.transform, M_PI, 0, 1, 0);
[textLayer_ setNeedsDisplay];
For test i've rotated text separately during the initialization.
// init text
textLayer_ = [CATextLayer layer];
…
textLayer_.transform = CATransform3DRotate(textLayer_.transform, M_PI, 0, 1, 0);
textLayer_.contentsScale = [[UIScreen mainScreen] scale];
Text can be rotated and remains clear
image here
Rasterizing
What's probably happening here is that it decides it has to render the textLayer to pixels. Note the warning for shouldRasterize in the CALayer Class Reference:
When the value of this property is NO, the layer is composited directly into the destination whenever possible. The layer may still be rasterized prior to compositing if certain features of the compositing model (such as the inclusion of filters) require it.
So, CATextLayer may suddenly decide to rasterize. It decides to rasterize if it's a sublayer of a rotated layer. So, don't make that happen.
Single-Sided Layers
That takes you back to your solution that causes the reversed text. You can prevent this by turning off doubleSided on the text layers. Your signs will now be blank on the far side, so add a second text layer, rotated 180 degrees relative to the first.
Declare two text layers:
#property (retain) CAShapeLayer *pathLayer;
#property (retain) CATextLayer *textLayerFront;
#property (retain) CATextLayer *textLayerBack;
Then, initialize them to be single-sided, with the back layer rotated 180 degrees:
CAShapeLayer *pathLayer = [CAShapeLayer layer];
// Also need to store a UIBezierPath in the pathLayer.
CATextLayer *textLayerFront = [CATextLayer layer];
textLayerFront.doubleSided = NO;
textLayerFront.string = #"Front";
textLayerFront.contentsScale = [[UIScreen mainScreen] scale];
CATextLayer *textLayerBack = [CATextLayer layer];
textLayerBack.doubleSided = NO;
// Eventually both sides will have the same text, but for demonstration purposes we will label them differently.
textLayerBack.string = #"Back";
// Rotate the back layer 180 degrees relative to the front layer.
textLayerBack.transform = CATransform3DRotate(textLayerBack.transform, M_PI, 0, 1, 0);
textLayerBack.contentsScale = [[UIScreen mainScreen] scale];
// Make all the layers siblings. These means they must all be rotated independently of each other.
// The layers can flicker if their Z position is close to the background, so move them forward.
// This will not work if the main layer has a perspective transform on it.
textLayerFront.zPosition = 256;
textLayerBack.zPosition = 256;
// It would make sense to make the text layers siblings of the path layer, but this seems to mean they get pre-rendered, blurring them.
[self.layer addSublayer:pathLayer];
[self.layer addSublayer:textLayerBack];
[self.layer addSublayer:textLayerFront];
// Store the layers constructed at this time for later use.
[self setTextLayerFront:textLayerFront];
[self setTextLayerBack:textLayerBack];
[self setPathLayer:pathLayer];
You can then rotate the layers. They will appear correct as long as you always rotate by the same amount.
CGFloat angle = M_PI;
self.pathLayer.transform = CATransform3DRotate(self.pathLayer.transform, angle, 0, 1, 0);
self.textLayerFront.transform = CATransform3DRotate(self.textLayerFront.transform, angle, 0, 1, 0);
self.textLayerBack.transform = CATransform3DRotate(self.textLayerBack.transform, angle, 0, 1, 0);
You should then find that you can rotate your sign to any angle while the text remains sharp.
Text to Path
There is an alternative, if you really need to manipulate your text display in ways that cause CATextLayer to rasterize: convert the text to a UIBezierPath representation. This can then be placed in a CAShapeLayer. Doing so requires delving deep into Core Text, but the results are powerful. For example, you can animate the text being drawn.
// - (UIBezierPath*) bezierPathWithString:(NSString*) string font:(UIFont*) font inRect:(CGRect) rect;
// Requires CoreText.framework
// This creates a graphical version of the input screen, line wrapped to the input rect.
// Core Text involves a whole hierarchy of objects, all requiring manual management.
- (UIBezierPath*) bezierPathWithString:(NSString*) string font:(UIFont*) font inRect:(CGRect) rect;
{
UIBezierPath *combinedGlyphsPath = nil;
CGMutablePathRef combinedGlyphsPathRef = CGPathCreateMutable();
if (combinedGlyphsPathRef)
{
// It would be easy to wrap the text into a different shape, including arbitrary bezier paths, if needed.
UIBezierPath *frameShape = [UIBezierPath bezierPathWithRect:rect];
// If the font name wasn't found while creating the font object, the result is a crash.
// Avoid this by falling back to the system font.
CTFontRef fontRef;
if ([font fontName])
fontRef = CTFontCreateWithName((__bridge CFStringRef) [font fontName], [font pointSize], NULL);
else if (font)
fontRef = CTFontCreateUIFontForLanguage(kCTFontUserFontType, [font pointSize], NULL);
else
fontRef = CTFontCreateUIFontForLanguage(kCTFontUserFontType, [UIFont systemFontSize], NULL);
if (fontRef)
{
CGPoint basePoint = CGPointMake(0, CTFontGetAscent(fontRef));
CFStringRef keys[] = { kCTFontAttributeName };
CFTypeRef values[] = { fontRef };
CFDictionaryRef attributesRef = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values,
sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
if (attributesRef)
{
CFAttributedStringRef attributedStringRef = CFAttributedStringCreate(NULL, (__bridge CFStringRef) string, attributesRef);
if (attributedStringRef)
{
CTFramesetterRef frameSetterRef = CTFramesetterCreateWithAttributedString(attributedStringRef);
if (frameSetterRef)
{
CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetterRef, CFRangeMake(0,0), [frameShape CGPath], NULL);
if (frameRef)
{
CFArrayRef lines = CTFrameGetLines(frameRef);
CFIndex lineCount = CFArrayGetCount(lines);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, lineCount), lineOrigins);
for (CFIndex lineIndex = 0; lineIndex<lineCount; lineIndex++)
{
CTLineRef lineRef = CFArrayGetValueAtIndex(lines, lineIndex);
CGPoint lineOrigin = lineOrigins[lineIndex];
CFArrayRef runs = CTLineGetGlyphRuns(lineRef);
CFIndex runCount = CFArrayGetCount(runs);
for (CFIndex runIndex = 0; runIndex<runCount; runIndex++)
{
CTRunRef runRef = CFArrayGetValueAtIndex(runs, runIndex);
CFIndex glyphCount = CTRunGetGlyphCount(runRef);
CGGlyph glyphs[glyphCount];
CGSize glyphAdvances[glyphCount];
CGPoint glyphPositions[glyphCount];
CFRange runRange = CFRangeMake(0, glyphCount);
CTRunGetGlyphs(runRef, CFRangeMake(0, glyphCount), glyphs);
CTRunGetPositions(runRef, runRange, glyphPositions);
CTFontGetAdvancesForGlyphs(fontRef, kCTFontDefaultOrientation, glyphs, glyphAdvances, glyphCount);
for (CFIndex glyphIndex = 0; glyphIndex<glyphCount; glyphIndex++)
{
CGGlyph glyph = glyphs[glyphIndex];
// For regular UIBezierPath drawing, we need to invert around the y axis.
CGAffineTransform glyphTransform = CGAffineTransformMakeTranslation(lineOrigin.x+glyphPositions[glyphIndex].x, rect.size.height-lineOrigin.y-glyphPositions[glyphIndex].y);
glyphTransform = CGAffineTransformScale(glyphTransform, 1, -1);
CGPathRef glyphPathRef = CTFontCreatePathForGlyph(fontRef, glyph, &glyphTransform);
if (glyphPathRef)
{
// Finally carry out the appending.
CGPathAddPath(combinedGlyphsPathRef, NULL, glyphPathRef);
CFRelease(glyphPathRef);
}
basePoint.x += glyphAdvances[glyphIndex].width;
basePoint.y += glyphAdvances[glyphIndex].height;
}
}
basePoint.x = 0;
basePoint.y += CTFontGetAscent(fontRef) + CTFontGetDescent(fontRef) + CTFontGetLeading(fontRef);
}
CFRelease(frameRef);
}
CFRelease(frameSetterRef);
}
CFRelease(attributedStringRef);
}
CFRelease(attributesRef);
}
CFRelease(fontRef);
}
// Casting a CGMutablePathRef to a CGPathRef seems to be the only way to convert what was just built into a UIBezierPath.
combinedGlyphsPath = [UIBezierPath bezierPathWithCGPath:(CGPathRef) combinedGlyphsPathRef];
CGPathRelease(combinedGlyphsPathRef);
}
return combinedGlyphsPath;
}
Here is rotating outlined text, created with the method above. It was also possible to add perspective without the z positions of the text layers becoming apparent.
This worked for me:
myTextLayer.contentsScale = UIScreen.mainScreen.scale;
The text will render crisp even when transformed.