I'm currently writing an application against the iOS 6.1 SDK. I know that some things in iOS 7 may obviate the need for a solution to my question but in the interest of learning I'm going to ask anyway.
The app will consist of a table view and custom table view cells. I'd like the only subview of the cell's contentView to be a custom view with an NSAttributedString drawn using Core Text. Since each cell's string will be different, the glyph positioning needs to be dependent on the number of glyphs (i.e. longer strings will have less visible space between glyphs). The size of the font and the physical bounds must remain the same it is only the glyph positioning that will be different.
I have the following code that for whatever reason does not do what I expect.
Here is the .h for the BMPTeamNameView - custom view (subview of contentView)
@interface BMPTeamNameView : UIView
-(id)initWithFrame:(CGRect)frame text:(NSString *)text textInset:(UIEdgeInsets)insets font:(UIFont *)font;
@property (nonatomic, copy) NSAttributedString *attributedString;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) UIEdgeInsets insets;
@end
The new designated initializer will now set the frame, the text to use for the attributed string, the insets to determine the text rect with respect to contentView rect, and the font to use.
Originally in my custom drawRect: I used a CTFramesetterRef, however a CTFramesetterRef will create an immutable frame which (may have?) restricted the laying out of individual glyphs. In this implementation I use a CTTypesetterRef to create the CTLineRef. Using a CTFrame leads to different drawing behavior when you compare CTLineDraw() and CTFrameDraw() but that is for another question. My drawRect: is as follows:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// Flips the coordinates so that drawing will be right side up
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// Path that will hold the textRect
CGMutablePathRef path = CGPathCreateMutable();
// rectForTextInsets: returns a rect based on the insets with respect to cell contentView
self.textRect = [self rectForTextWithInsets:self.insets];
// Path adding / sets color for drawing
CGPathAddRect(path, NULL, self.textRect);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextAddPath(context, path);
CGContextFillPath(context);
// convenience method to return dictionary of attributes for string
NSDictionary *attributes = [self attributesForAttributedString];
// convenience method returns "Hello World" with attributes
// typesetter property is set in the custom setAttributedString:
self.attributedString = [self attributedStringWithAttributes:attributes];
CTTypesetterRef typesetter = self.typesetter;
// Creates the line for the attributed string
CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(0, 0));
CGPoint *positions = NULL;
CGGlyph *glyphs = NULL;
CGPoint *positionsBuffer = NULL;
CGGlyph *glyphsBuffer = NULL;
// We will only have one glyph run
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
CTRunRef glyphRun = CFArrayGetValueAtIndex(glyphRuns, 0);
// Get the count of all the glyphs
NSUInteger glyphCount = CTRunGetGlyphCount(glyphRun);
// This function gets the ptr to the glyphs, may return NULL
glyphs = (CGGlyph *)CTRunGetGlyphsPtr(glyphRun);
if (glyphs == NULL) {
// if glyphs is NULL allocate a buffer for them
// store them in the buffer
// set the glyphs ptr to the buffer
size_t glyphsBufferSize = sizeof(CGGlyph) * glyphCount;
CGGlyph *glyphsBuffer = malloc(glyphsBufferSize);
CTRunGetGlyphs(glyphRun, CFRangeMake(0, 0), glyphsBuffer);
glyphs = glyphsBuffer;
}
// This function gets the ptr to the positions, may return NULL
positions = (CGPoint *)CTRunGetPositionsPtr(glyphRun);
if (positions == NULL) {
// if positions is NULL allocate a buffer for them
// store them in the buffer
// set the positions ptr to the buffer
size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
CGPoint *positionsBuffer = malloc(positionsBufferSize);
CTRunGetPositions(glyphRun, CFRangeMake(0, 0), positionsBuffer);
positions = positionsBuffer;
}
// Changes each x by 15 and then sets new value at array index
for (int i = 0; i < glyphCount; i++) {
NSLog(@"positionAtIndex: %@", NSStringFromCGPoint(positions[i]));
CGPoint oldPosition = positions[i];
CGPoint newPosition = CGPointZero;
NSLog(@"oldPosition = %@", NSStringFromCGPoint(oldPosition));
newPosition.x = oldPosition.x + 15.0f;
newPosition.y = oldPosition.y;
NSLog(@"newPosition = %@", NSStringFromCGPoint(newPosition));
positions[i] = newPosition;
NSLog(@"positionAtIndex: %@", NSStringFromCGPoint(positions[i]));
}
// When CTLineDraw is commented out this will not display the glyphs on the screen
CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
// When CGContextShowGlyphsAtPositions is commented out...
// This will draw the string however it aligns the text to the view's lower left corner
// CTFrameDraw would draw the text properly in the view's upper left corner
// This is the difference I was speaking of and I'm not sure why it is
CTLineDraw(line, context);
// Make sure to release any CF objects and release allocated buffers
CFRelease(path);
free(positionsBuffer);
free(glyphsBuffer);
}
I'm not sure exactly why CGContextShowGlyphsAtPositions() is not displaying the glyphs properly or why CTLineDraw() will not make use of the new glyph positions. Am I handling the allocation of those positions and glyphs incorrectly? Caveman debugging shows that the glyphs are as expected and the positions are being changed. I know that my code did not satisfy exactly what I was looking for (I was changing glyph position by 15.0f rather than based on string) however, where am I going wrong in laying out these glyphs?
Glyphs are units of visual rendering. A set of glyphs makes up a font . Glyphs can be construed as the basic units of organization of the visual rendering of text, just as characters are the basic unit of organization of encoded text.
Create text layouts, optimize font handling, and access font metrics and glyph data. Core Text provides a low-level programming interface for laying out text and handling fonts. The Core Text layout engine is designed for high performance, ease of use, and close integration with Core Foundation.
Core Text is an advanced, low-level technology for laying out text and handling fonts. The Core Text API, introduced in Mac OS X v10.5 and iOS 3.2, is accessible from all OS X and iOS environments. Important: Core Text is intended for developers who must do text layout and font handling at a low level, such as developers of layout engines.
Core Text provides a low-level programming interface for laying out text and handling fonts. The Core Text layout engine is designed for high performance, ease of use, and close integration with Core Foundation.
First, if you want to just want to tighten or loosen character spacing, you don't need Core Text for that. Just attach NSKernAttributeName
to the section of your attributed string that you want to adjust. Positive to loosen, negative to tighten. (Zero means "no kerning," which is different from "default kerning." To get default kerning, don't set this attribute.) You can use size
on NSAttributedString
to try different spacings until you get the size you want.
"Positions" don't work the way you think they do. Positions are not screen coordinates. They're relative to the current text origin, which is the lower-left corner of the current line. (Remember that CoreText coordinates are upside down from UIKit coordinates.) You need to call CGContextSetTextPosition()
before calling CGContextShowGlyphsAtPositions()
. CTLineDraw()
is a wrapper around CGContextShowGlyphsAtPositions()
. That's why CTLineDraw()
is drawing in the lower left (0,0). CTFrameDraw
is drawing in the upper left because it adjusts the text origin correctly.
When you call CGContextShowGlyphsAtPositions()
directly, it looks like nothing draws. catlan's answer addresses this. You need to set the context's font and color before drawing.
Your reposition code doesn't really do anything useful. It moves all the text 15 points to the right, but it doesn't actually change the spacing between them. (If that's all you wanted then you could just draw the string 15 points to the right.)
Your current code leaks memory. You allocate positionsBuffer
and glyphsBuffer
, but these shadow the previously declared versions. So you're always calling free(NULL)
at the end.
For a full example of doing this kind of text adjustment by hand, see PinchTextLayer. But it can almost certainly be solved better by adjusting the kerning in the attributed string.
CTLineDraw
will use the font and color information from the CFAttributedString
.
CGContextShowGlyphsAtPositions
on the other hand needs these to be set on the CGContext
:
CGFontRef cgFont = CTFontCopyGraphicsFont(font, NULL);
CGContextSetFont(context, cgFont);
CGContextSetFontSize(context, CTFontGetSize(font));
CGContextSetFillColorWithColor(context, fillColor);
CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
CFRelease(cgFont)
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