Resize CATextLayer to fit text on iOS - ios4

All my research so far seems to indicate it is not possible to do this accurately. The only two options available to me at the outset were:
a) Using a Layout manager for the CATextLayer - not available on iOS as of 4.0
b) Use sizeWithFont:constrainedToSize:lineBreakMode: and adjust the frame of the CATextLayer according to the size returned here.
Option (b), being the simplest approach, should work. After all, it works perfectly with UILabels. But when I applied the same frame calculation to CATextLayer, the frame was always turning out to be a bit bigger than expected or needed.
As it turns out, the line-spacing in CATextLayers and UILabels (for the same font and size) is different. As a result, sizeWithFont (whose line-spacing calculations would match with that of UILabels) does not return the expected size for CATextLayers.
This is further proven by printing the same text using a UILabel, as against a CATextLayer and comparing the results. The text in the first line overlaps perfectly (it being the same font), but the line-spacing in CATextLayer is just a little shorter than in UILabel. (Sorry I can't upload a screenshot right now as the ones I already have contain confidential data, and I presently don't have the time to make a sample project to get clean screenshots. I'll upload them later for posterity, when I have the time)
This is a weird difference, but I thought it would be possible to adjust the spacing in the CATextLayer by specifying the appropriate attribute for the NSAttributedString I use there, but that does not seem to be the case. Looking into CFStringAttributes.h I can't find a single attribute that could be related to line-spacing.
Bottomline:
So it seems like it's not possible to use CATextLayer on iOS in a scenario where the layer is required to fit to its text. Am I right on this or am I missing something?
P.S:
The reason I wanted to use CATextLayer and NSAttributedString's is because the string to be displayed is to be colored differently at different points. I guess I'd just have to go back to drawing the strings by hand as always....of course there's always the option of hacking the results from sizeWithFont to get the proper line-height.
Abusing the 'code' tags a little to make the post more readable.
I'm not able to tag the post with 'CATextLayer' - surprisingly no such tags exist at the moment. If someone with enough reputation bumps into this post, please tag it accordingly.

Try this:
- (CGFloat)boundingHeightForWidth:(CGFloat)inWidth withAttributedString:(NSAttributedString *)attributedString {
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attributedString);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(inWidth, CGFLOAT_MAX), NULL);
CFRelease(framesetter);
return suggestedSize.height;
}
You'll have to convert your NSString to NSAttributedString. In-case of CATextLayer, you can use following CATextLayer subclass method:
- (NSAttributedString *)attributedString {
// If string is an attributed string
if ([self.string isKindOfClass:[NSAttributedString class]]) {
return self.string;
}
// Collect required parameters, and construct an attributed string
NSString *string = self.string;
CGColorRef color = self.foregroundColor;
CTFontRef theFont = self.font;
CTTextAlignment alignment;
if ([self.alignmentMode isEqualToString:kCAAlignmentLeft]) {
alignment = kCTLeftTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentRight]) {
alignment = kCTRightTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentCenter]) {
alignment = kCTCenterTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentJustified]) {
alignment = kCTJustifiedTextAlignment;
} else if ([self.alignmentMode isEqualToString:kCAAlignmentNatural]) {
alignment = kCTNaturalTextAlignment;
}
// Process the information to get an attributed string
CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
if (string != nil)
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (CFStringRef)string);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTForegroundColorAttributeName, color);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, theFont);
CTParagraphStyleSetting settings[] = {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);
NSMutableAttributedString *ret = (NSMutableAttributedString *)attrString;
return [ret autorelease];
}
HTH.

I have a much easier solution, that may or may not work for you.
If you aren't doing anything special with the CATextLayer that you can't do a UILabel, instead make a CALayer and add the layer of the UILabel to the CALayer
UILabel*label = [[UILabel alloc]init];
//Do Stuff to label
CALayer *layer = [CALayer layer];
//Set Size/Position
[layer addSublayer:label.layer];
//Do more stuff to layer

With LabelKit you don't need CATextLayer anymore. No more wrong line spacing and wider characters, all is drawn in the same way as UILabel does, while still animated.

This page gave me enough to create a simple centered horizontally CATextLayer : http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
- (void)drawInContext:(CGContextRef)ctx {
CGFloat height, fontSize;
height = self.bounds.size.height;
fontSize = self.fontSize;
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, 0.0, (fontSize-height)/2.0 * -1.0);
[super drawInContext:ctx];
CGContextRestoreGState(ctx);
}

Related

Extendscript: How to check whether text content overflows the containing rectangle

I am using Extendscript for Photoshop CS5 to change the text of a text layer. Is there a way of checking whether the text fits e.g. by checking whether it overflows after changing the content?
I created a solution that works perfectly fine :). Maybe someone else can use it as well. Let me know if it works for you too!
function scaleTextToFitBox(textLayer) {
var fitInsideBoxDimensions = getLayerDimensions(textLayer);
while(fitInsideBoxDimensions.height < getRealTextLayerDimensions(textLayer).height) {
var fontSize = parseInt(textLayer.textItem.size);
textLayer.textItem.size = new UnitValue(fontSize * 0.95, "px");
}
}
function getRealTextLayerDimensions(textLayer) {
var textLayerCopy = textLayer.duplicate(activeDocument, ElementPlacement.INSIDE);
textLayerCopy.textItem.height = activeDocument.height;
textLayerCopy.rasterize(RasterizeType.TEXTCONTENTS);
var dimensions = getLayerDimensions(textLayerCopy);
textLayerCopy.remove();
return dimensions;
}
function getLayerDimensions(layer) {
return {
width: layer.bounds[2] - layer.bounds[0],
height: layer.bounds[3] - layer.bounds[1]
};
}
How to use / Explanation
Create a text layer that has a defined width and height.
You can change the text layers contents and then call scaleTextToFitBox(textLayer);
The function will change the text/font size until the text fits inside the box (so that no text is invisible)!
The script decreases the font size by 5% (* 0.95) each step until the texts fits inside the box. You can change the multiplier to achieve a more precise result or to increase performance.
I haven't found a way to do this directly. But I've used the following technique to determine the height I needed for a textbox (I wanted to keep the width constant) before.
expand the textbox's height well beyond what is needed to accommodate the text inside it.
duplicate the layer
rasterize the duplicate
measure the bounds of the rasterized layer.
adjust the bounds of the original text layer as needed
delete the rasterized duplicate
Totally roundabout - but it did work.

UItextview will disregard line height when NSFontAttributeName is set

Well, Ive searched in several places and although some people allegedly have found fixes it doesn't seem to apply to my case.
I'm trying to procedurally set the line height of a few UItextviews like this :
UITextView *lab = [LocalTexts objectAtIndex:j];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineHeightMultiple = 50.0f;
paragraphStyle.maximumLineHeight = 50.0f;
paragraphStyle.minimumLineHeight = 50.0f;
NSString *string = lab.text;
NSDictionary *ats = #{
NSFontAttributeName : [UIFont fontWithName:#"DIN Medium" size:16.0f],
NSParagraphStyleAttributeName : paragraphStyle,
};
lab.attributedText = [[NSAttributedString alloc] initWithString:string attributes:ats];
Strange thing is that if I disable the NSFontAttributeName assignment, the line height will work, also, if I set the Paragraph style to have a certain paragraph height, that always works too, so the NSParagraphStyleAttribute IS NOT being fully ignored. I dont know if it is a bug or I'm actually doing something wrong.
I tried implementing it as pure CORE TEXT, but it is a bit too complex for the current scope of the project.
Hope someone can point me in the right direction. Thanks.
This is a known bug in NSHTMLWriter which is used by UITextView to convert your attributed string to HTML: http://www.cocoanetics.com/2012/12/radar-uitextview-ignores-minimummaximum-line-height-in-attributed-string/
You can use UITextView replacement we have in DTCoreText to render this text correctly: https://github.com/Cocoanetics/DTCoreText

UITextView attributedText and syntax highlighting

Background
So, with iOS 6 an UITextView can take an attributedString, which could be useful for Syntax highlighting.
I'm doing some regex patterns in -textView:shouldChangeTextInRange:replacementText: and oftentimes I need to change the color of a word already typed. I see no other options than resetting the attributedText, which takes time.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
//A context will allow us to not call -attributedText on the textView, which is slow.
//Keep context up to date
[self.context replaceCharactersInRange:range withAttributedString:[[NSAttributedString alloc] initWithString:text attributes:self.textView.typingAttributes]];
// […]
self.textView.scrollEnabled = FALSE;
[self.context setAttributes:self.defaultStyle range:NSMakeRange(0, self.context.length)];
[self refresh]; //Runs regex-patterns in the context
  textView.attributedText = self.context;
self.textView.selectedRange = NSMakeRange(range.location + text.length, 0);
self.textView.scrollEnabled = TRUE;
return FALSE;
}
This runs okayish on the simulator, but on an iPad 3 each -setAttributedText takes a few hundreds of milliseconds.
I filed a bug to Apple, with the request of being able to mutate the attributedText. It got marked as a duplicate, so I cannot see what they're saying about this.
The question
The more specific question:
How can I change the color of certain ranges in a UITextView, with a large multicolored text, with good enough performance to do it in every shouldReplaceText...?
The more broad question:
How do you do syntax highlighting with a UITextView in iOS 6?
I encountered the same problem for my application Zap-Guitar (No-Strings-Attached) where I allow users to type/paste/edit their own songs and the app highlights recognized chords.
Yes it is true apple uses an html writer and parser to display the attributed text. A wonderful explanation of behind the scene can be found here: http://www.cocoanetics.com/2012/12/uitextview-caught-with-trousers-down/
The only solution I found for this problem is not to use attributed text which is an overkill for syntax highlighting.
Instead I reverted to the good old UITextView with plain text and added buttons to the text view where highlighted was needed. To compute the buttons frames I used this answer: How to find position or get rect of any word in textview and place buttons over that?
This reduced CPU usage by 30% (give or take).
Here is a handy category:
#implementation UITextView (WithButtons)
- (CGRect)frameForTextRange:(NSRange)range {
UITextPosition *beginning = self.beginningOfDocument;
UITextPosition *start = [self positionFromPosition:beginning offset:range.location];
UITextPosition *end = [self positionFromPosition:start offset:range.length];
UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
CGRect rect = [self firstRectForRange:textRange];
return [self convertRect:rect fromView:self.textInputView];
}
#end
The attributedText accessors have to round-trip to/from HTML, so it's really non-optimal for a syntax-highlighted text view implementation. On iOS 6, you'll probably want to use CoreText directly.

How to adjust size of UITextView automatically ?

I want to make speech bubbles but there is a problem adjusting size of the bubbles in UITextView.
is there a way to increase the size of the Textview automatically depending on the length of the
text by using UITextView?
Okay I found a better way of doing what I was suggesting. Rather then deal with the UIView and resizing depending on the text inside it. I just made the UITextView have a rounded edge which now looks like a panel. Perfect. Hope this helps someone!
If your interested in the code
- (void)drawRect:(CGRect)rect
{
CGRect frame = self.frame;
frame.size.height = self.contentSize.height;
self.frame = frame; // Drawing code
self.clipsToBounds = YES;
self.layer.cornerRadius = 10.0f;
}
I think I understand what your getting at. I had this problem the other day. This bit of code will adjust the UITextView contents. Create a new class and call this into it via a new class. If you want to adjust the View well I'm still working on that :-P
Hope this helps
- (void)drawRect:(CGRect)rect
{
CGRect frame = self.frame;
frame.size.height = self.contentSize.height;
self.frame = frame; // Drawing code
}

How to get text in a CATextLayer to be clear

I've made a CALayer with an added CATextLayer and the text comes out blurry. In the docs, they talk about "sub-pixel antialiasing", but that doesn't mean much to me. Anyone have a code snippet that makes a CATextLayer with a bit of text that is clear?
Here's the text from Apple's documentation:
Note: CATextLayer disables sub-pixel antialiasing when rendering text.
Text can only be drawn using sub-pixel antialiasing when it is
composited into an existing opaque background at the same time that
it's rasterized. There is no way to draw subpixel-antialiased text by
itself, whether into an image or a layer, separately in advance of
having the background pixels to weave the text pixels into. Setting
the opacity property of the layer to YES does not change the rendering
mode.
The second sentence implies that one can get good looking text if one composites it into an existing opaque background at the same time that it's rasterized. That's great, but how do I composite it and how do you give it an opaque background and how do you rasterize it?
The code they use in their example of a Kiosk Menu is as such: (It's OS X, not iOS, but I assume it works!)
NSInteger i;
for (i=0;i<[names count];i++) {
CATextLayer *menuItemLayer=[CATextLayer layer];
menuItemLayer.string=[self.names objectAtIndex:i];
menuItemLayer.font=#"Lucida-Grande";
menuItemLayer.fontSize=fontSize;
menuItemLayer.foregroundColor=whiteColor;
[menuItemLayer addConstraint:[CAConstraint
constraintWithAttribute:kCAConstraintMaxY
relativeTo:#"superlayer"
attribute:kCAConstraintMaxY
offset:-(i*height+spacing+initialOffset)]];
[menuItemLayer addConstraint:[CAConstraint
constraintWithAttribute:kCAConstraintMidX
relativeTo:#"superlayer"
attribute:kCAConstraintMidX]];
[self.menuLayer addSublayer:menuItemLayer];
} // end of for loop
Thanks!
EDIT: Adding the code that I actually used that resulted in blurry text. It's from a related question I posted about adding a UILabel rather than a CATextLayer but getting a black box instead. http://stackoverflow.com/questions/3818676/adding-a-uilabels-layer-to-a-calayer-and-it-just-shows-black-box
CATextLayer* upperOperator = [[CATextLayer alloc] init];
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
CGFloat components1[4] = {1.0, 1.0, 1.0, 1.0};
CGColorRef almostWhite = CGColorCreate(space,components1);
CGFloat components2[4] = {0.0, 0.0, 0.0, 1.0};
CGColorRef almostBlack = CGColorCreate(space,components2);
CGColorSpaceRelease(space);
upperOperator.string = [NSString stringWithFormat:#"13"];
upperOperator.bounds = CGRectMake(0, 0, 100, 50);
upperOperator.foregroundColor = almostBlack;
upperOperator.backgroundColor = almostWhite;
upperOperator.position = CGPointMake(50.0, 25.0);
upperOperator.font = #"Helvetica-Bold";
upperOperator.fontSize = 48.0f;
upperOperator.borderColor = [UIColor redColor].CGColor;
upperOperator.borderWidth = 1;
upperOperator.alignmentMode = kCAAlignmentCenter;
[card addSublayer:upperOperator];
[upperOperator release];
CGColorRelease(almostWhite);
CGColorRelease(almostBlack);
EDIT 2: See my answer below for how this got solved. sbg.
Short answer — You need to set the contents scaling:
textLayer.contentsScale = [[UIScreen mainScreen] scale];
A while ago I learned that when you have custom drawing code, you have to check for the retina display and scale your graphics accordingly. UIKit takes care of most of this, including font scaling.
Not so with CATextLayer.
My blurriness came from having a .zPosition that was not zero, that is, I had a transform applied to my parent layer. By setting this to zero, the blurriness went away, and was replaced by serious pixelation.
After searching high and low, I found that you can set .contentsScale for a CATextLayer and you can set it to [[UIScreen mainScreen] scale] to match the screen resolution. (I assume this works for non-retina, but I haven't checked - too tired)
After including this for my CATextLayer the text became crisp. Note - it's not necessary for the parent layer.
And the blurriness? It comes back when you're rotating in 3D, but you don't notice it because the text starts out clear and while it's in motion, you can't tell.
Problem solved!
Swift
Set the text layer to use the same scale as the screen.
textLayer.contentsScale = UIScreen.main.scale
Before:
After:
Before setting shouldRasterize, you should:
set the rasterizationScale of the base layer you are going to rasterize
set the contentsScale property of any CATextLayers and possibly other types of layers(it never hurts to do it)
If you don't do #1, then the retina version of sub layers will look blurry, even for normal CALayers.
- (void)viewDidLoad {
[super viewDidLoad];
CALayer *mainLayer = [[self view] layer];
[mainLayer setRasterizationScale:[[UIScreen mainScreen] scale]];
CATextLayer *messageLayer = [CATextLayer layer];
[messageLayer setForegroundColor:[[UIColor blackColor] CGColor]];
[messageLayer setContentsScale:[[UIScreen mainScreen] scale]];
[messageLayer setFrame:CGRectMake(50, 170, 250, 50)];
[messageLayer setString:(id)#"asdfasd"];
[mainLayer addSublayer:messageLayer];
[mainLayer setShouldRasterize:YES];
}
First off I wanted to point out that you've tagged your question with iOS, but constraint managers are only available on OSX, so I'm not sure how you're getting this to work unless you've been able to link against it in the simulator somehow. On the device, this functionality is not available.
Next, I'll just point out that I create CATextLayers often and never have the blurring problem you're referring to so I know it can be done. In a nutshell this blurring occurs because you are not positioning your layer on the whole pixel. Remember that when you set the position of a layer, you use a float values for the x and y. If those values have numbers after the decimal, the layer will not be positioned on the whole pixel and will therefore give this blurring effect--the degree of which depending upon the actual values. To test this, just create a CATextLayer and explicitly add it to the layer tree ensuring that your position parameter is on a whole pixel. For example:
CATextLayer *textLayer = [CATextLayer layer];
[textLayer setBounds:CGRectMake(0.0f, 0.0f, 200.0f, 30.0f)];
[textLayer setPosition:CGPointMake(200.0f, 100.0f)];
[textLayer setString:#"Hello World!"];
[[self menuLayer] addSublayer:textLayer];
If your text is still blurry, then there is something else wrong. Blurred text on your text layer is an artifact of incorrectly written code and not an intended capability of the layer. When adding your layers to a parent layer, you can just coerce the x and y values to the nearest whole pixel and it should solve your blurring problem.
You should do 2 things, the first was mentioned above:
Extend CATextLayer and set the opaque and contentsScale properties to properly support retina display, then render with anti aliasing enabled for text.
+ (TextActionLayer*) layer
{
TextActionLayer *layer = [[TextActionLayer alloc] init];
layer.opaque = TRUE;
CGFloat scale = [[UIScreen mainScreen] scale];
layer.contentsScale = scale;
return [layer autorelease];
}
// Render after enabling with anti aliasing for text
- (void)drawInContext:(CGContextRef)ctx
{
CGRect bounds = self.bounds;
CGContextSetFillColorWithColor(ctx, self.backgroundColor);
CGContextFillRect(ctx, bounds);
CGContextSetShouldSmoothFonts(ctx, TRUE);
[super drawInContext:ctx];
}
If you came searching here for a similar issue for a CATextLayer in OSX, after much wall head banging, I got the sharp clear text by doing:
text_layer.contentsScale = self.window!.backingScaleFactor
(I also set the views background layer contentsScale to be the same).
This is faster and easier: you just need to set contentsScale
CATextLayer *label = [[CATextLayer alloc] init];
[label setFontSize:15];
[label setContentsScale:[[UIScreen mainScreen] scale]];
[label setFrame:CGRectMake(0, 0, 50, 50)];
[label setString:#"test"];
[label setAlignmentMode:kCAAlignmentCenter];
[label setBackgroundColor:[[UIColor clearColor] CGColor]];
[label setForegroundColor:[[UIColor blackColor] CGColor]];
[self addSublayer:label];

Resources