I was trying to create an animated text label and suddenly this odd problem hit me. I am trying to write out a simple label using CATextLayer on a view. As you can see, I have tried to calculate the frame of the text by using the sizeWithAttributes: of the NSAttributedString class. This gives out a frame that doesn't perfectly fit the CATextLayer even though I am displaying the same attributed string using the layer.
Any insights into why this odd font-rendering problem is happening would be appreciated.
#import "AppDelegate.h"
#implementation AppDelegate
#synthesize view;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
view.layer = [CALayer layer];
[view setWantsLayer:YES];
CATextLayer *textLayer = [CATextLayer layer];
textLayer.fontSize = 12;
textLayer.position = CGPointMake(100, 50);
[view.layer addSublayer:textLayer];
NSFont *font = [NSFont fontWithName:#"Helvetica-Bold" size:12];
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, nil];
NSString *helloString = #"This is a long string";
NSAttributedString *hello = [[NSAttributedString alloc] initWithString:helloString attributes:attrs];
NSSize stringBounds = [helloString sizeWithAttributes:attrs];
stringBounds.width = ceilf(stringBounds.width);
stringBounds.height = ceilf(stringBounds.height);
textLayer.bounds = CGRectMake(0, 0, stringBounds.width, stringBounds.height);
textLayer.string = hello;
}
#end
CATextLayer renders the glyphs using CoreText and not Cocoa (educated guess based on experiments).
Check out this sample code to see how to calculate the size using CoreText and a CTTypesetter. This gave us much more accurate results:
https://github.com/rhult/height-for-width
Related
I have found that a call to boundingRectWithSize is extremely incorrect, missing an entire additional line, when called with NSFontAttributeName : [UIFont preferredFontForTextStyle:UIFontTextStyleBody]. However, using the font [UIFont fontWithName:#"Helvetica Neue" size:17.f], it is just fine.
Here is test code showing the bug:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *measureText = #"I'm the king of Portmanteaus ... My students slow clap";
CGSize maxSize = CGSizeMake(226.f, CGFLOAT_MAX);
UIFont *targetFont = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
CGRect stringRect = [measureText boundingRectWithSize:maxSize
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:#{ NSFontAttributeName : targetFont }
context:nil];
UILabel *drawLabel = [[UILabel alloc] initWithFrame:stringRect];
drawLabel.font = targetFont;
[self.view addSubview:drawLabel];
drawLabel.textColor = [UIColor blackColor];
drawLabel.text = measureText;
drawLabel.numberOfLines = 0;
}
And the output:
However, if I make targetFont = [UIFont fontWithName:#"Helvetica Neue" size:17.f];
It renders properly:
In the first case, the size is {226.20200000000008, 42.281000000000006}, but in the correct second case the size is {228.64999999999998, 60.366999999999997}. Therefore, this is not a rounding issue; this is missing an entire new line.
Is it wrong to pass [UIFont preferredFontForTextStyle:UIFontTextStyleBody] as an argument into boundingRectWithSize?
Edit
Per the comments in the answer below, I believe there is a bug with how iOS treats three periods in connection with the proceeding word. I have added this code:
measureText = [measureText stringByReplacingOccurrencesOfString:#" ..." withString:#"…"];
and it works properly.
A label adds a little margin round the outside of the text, and you have not allowed for that.
If, instead, you use a custom UIView exactly the size of the string rect, you will see that the text fits perfectly:
Here's the code I used. In the view controller:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *measureText = #"I'm the king of Portmanteaus ... My students slow clap";
CGSize maxSize = CGSizeMake(226.f, CGFLOAT_MAX);
UIFont *targetFont = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
CGRect stringRect = [measureText boundingRectWithSize:maxSize
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:#{ NSFontAttributeName : targetFont }
context:nil];
stringRect.origin = CGPointMake(100,100);
StringDrawer* sd = [[StringDrawer alloc] initWithFrame:stringRect];
[self.view addSubview:sd];
[sd draw:[[NSAttributedString alloc] initWithString:measureText attributes:#{ NSFontAttributeName : targetFont }]];
}
And here is String Drawer (a UIView subclass):
#interface StringDrawer()
#property (nonatomic, copy) NSAttributedString* text;
#end
#implementation StringDrawer
- (instancetype) initWithFrame: (CGRect) frame {
self = [super initWithFrame:frame];
self.backgroundColor = [UIColor yellowColor];
return self;
}
- (void) draw: (NSAttributedString*) text {
self.text = text;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
[self.text drawInRect:rect];
}
#end
Also, if your purpose is to make a label that contains the text by sizing its height, why not let Auto Layout do its thing? This next screen shot is a UILabel, with a width constraint 226, and no height constraint. I've assigned it your font and your text, in code. As you can see, it has sized itself to accommodate all the text:
That was achieved by this code, and no more was needed:
NSString *measureText = #"I'm the king of Portmanteaus ... My students slow clap";
UIFont *targetFont = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.lab.font = targetFont;
self.lab.text = measureText;
I'm new to cocoa and programming, sorry if this is something basic.
I'm using ARC. I have an NSImageView component, which is controlled by DropZone class. I drag & drop an image into it, but once I try to call in the scalling method I got, it tells me that the ": ImageIO: CGImageSourceCreateWithData data parameter is nil" I assume I'm doing something wrong, just don't know what yet.
Here's my method in DropZone.m
- (void)scaleIcons:(NSString *)outputPath{
NSImage *anImage;
NSSize imageSize;
NSString *finalPath;
anImage = [[NSImage alloc]init];
anImage = image;
imageSize = [anImage size];
imageSize.width = 512;
imageSize.height = 512;
[anImage setSize:imageSize];
finalPath = [outputPath stringByAppendingString:#"/icon_512x512.png"];
NSData *imageData = [anImage TIFFRepresentation];
NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:imageData];
NSData *dataToWrite = [rep representationUsingType:NSPNGFileType properties:nil];
[dataToWrite writeToFile:finalPath atomically:NO];
}
The DropZone.h
#interface DropZone : NSImageView <NSDraggingDestination>{
NSImage *image;
}
- (void)scaleIcons:(NSString *)outputPath;
#end
And here's how I call it it my AppDelegate.m
- (IBAction)createIconButtonClicked:(id)sender {
NSFileManager *filemgr;
NSString *tempDir;
filemgr = [NSFileManager defaultManager];
tempDir = [pathString stringByAppendingString:#"/icon.iconset"];
NSURL *newDir = [NSURL fileURLWithPath:tempDir];
[filemgr createDirectoryAtURL: newDir withIntermediateDirectories:YES attributes: nil error:nil];
DropZone *myZone = [[DropZone alloc] init];
[myZone scaleIcons:tempDir];
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:#"Done!"];
[alert runModal];
}
the image get's pulled from the pastebaord:
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender{
if ([NSImage canInitWithPasteboard:[sender draggingPasteboard]]) {
image = [[NSImage alloc]initWithPasteboard:[sender draggingPasteboard]];
[self setImage:image];
[self setImageAlignment: NSImageAlignCenter];
}
return YES;
}
For some reason my "image" gets lost. Could somebody help?
The problem is that you're creating a new instance of DropZone in your app delegate, but I'm assuming that you created the image view in IB and changed its class to DropZone. Is that correct? If so, you need to have an IBOutlet in the app delegate connected to the image view in IB, and have this:
[self.iv scaleIcons:tempDir];
where iv is my IBOutlet.
Without knowing where "image" comes from, I would say your problem is in these lines...
anImage = [[NSImage alloc]init];
anImage = image;
You should just grab the image from the imageView...
anImage = [myImageView image];
Hey I'm making an app that requires a certain part of a text view's text to be underlined. is there a simple way to do this like for making it bold and italics or must i make and import a custom font? thanks for the help in advance!
This is what i did. It works like butter.
1) Add CoreText.framework to your Frameworks.
2) import <CoreText/CoreText.h> in the class where you need underlined label.
3) Write the following code.
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:#"My Messages"];
[attString addAttribute:(NSString*)kCTUnderlineStyleAttributeName
value:[NSNumber numberWithInt:kCTUnderlineStyleSingle]
range:(NSRange){0,[attString length]}];
self.myMsgLBL.attributedText = attString;
self.myMsgLBL.textColor = [UIColor whiteColor];
#import <UIKit/UIKit.h>
#interface TextFieldWithUnderLine : UITextField
#end
#import "TextFieldWithUnderLine.h"
#implementation TextFieldWithUnderLine
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)drawRect:(CGRect)rect {
//Get the current drawing context
CGContextRef context = UIGraphicsGetCurrentContext();
//Set the line color and width
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetLineWidth(context, 0.5f);
//Start a new Path
CGContextBeginPath(context);
// offset lines up - we are adding offset to font.leading so that line is drawn right below the characters and still characters are visible.
CGContextMoveToPoint(context, self.bounds.origin.x, self.font.leading + 4.0f);
CGContextAddLineToPoint(context, self.bounds.size.width, self.font.leading + 4.0f);
//Close our Path and Stroke (draw) it
CGContextClosePath(context);
CGContextStrokePath(context);
}
#end
Since iOS 6.0 UILabel, UITextField and UITextView support displaying attributed strings using the attributedText property.
Usage:
NSMutableAttributedString *aStr = [[NSMutableAttributedString alloc] initWithString:#"text"];
[aStr addAttribute:NSUnderlineStyleAttributeName value:NSUnderlineStyleSingle range:NSMakeRange(0,2)];
label.attributedText = aStr;
I have a NSView, there is a NSImageView on the NSView
in the source codes of the NSView
I wrote:
NSImage *image = [[[NSImage alloc] initWithContentsOfURL:url] autorelease];
NSImageView *newImageView = nil;
newImageView = [[NSImageView alloc] initWithFrame:[self bounds]];
[newImageView setImage:image];
[newImageView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
Is there a way to read the width and height of the NSView and NSImageView?
The size of any view (NSView, NSImageView, etc.) is view.frame.size or view.bounds.size.
In both cases it is an identical NSSize struct. You can write code like this:
NSSize size = newImageView.frame.size;
NSLog(#"size: %#", NSStringFromSize(size));
NSLog(#"size: %f x %f", size.width, size.height);
To change it, you need to update the view frame property:
NSRect frame = view.frame;
frame.size = NSMakeSize(<#new width#>, <#new height#>);
view.frame = frame;
Try the - (NSRect)bounds method.
I am making an app that is a standalone menu item and the basis for the code is sample code I found on a website. The sample code uses a number as the menu icon, but I want to change it to an image.
I want it to be like other apps where it shows icon.png when not clicked and icon-active.png when clicked.
The current code is this:
- (void)drawRect:(NSRect)rect {
// Draw background if appropriate.
if (clicked) {
[[NSColor selectedMenuItemColor] set];
NSRectFill(rect);
}
// Draw some text, just to show how it's done.
NSString *text = #"3"; // whatever you want
NSColor *textColor = [NSColor controlTextColor];
if (clicked) {
textColor = [NSColor selectedMenuItemTextColor];
}
NSFont *msgFont = [NSFont menuBarFontOfSize:15.0];
NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
[paraStyle setParagraphStyle:[NSParagraphStyle defaultParagraphStyle]];
[paraStyle setAlignment:NSCenterTextAlignment];
[paraStyle setLineBreakMode:NSLineBreakByTruncatingTail];
NSMutableDictionary *msgAttrs = [NSMutableDictionary dictionaryWithObjectsAndKeys:
msgFont, NSFontAttributeName,
textColor, NSForegroundColorAttributeName,
paraStyle, NSParagraphStyleAttributeName,
nil];
[paraStyle release];
NSSize msgSize = [text sizeWithAttributes:msgAttrs];
NSRect msgRect = NSMakeRect(0, 0, msgSize.width, msgSize.height);
msgRect.origin.x = ([self frame].size.width - msgSize.width) / 2.0;
msgRect.origin.y = ([self frame].size.height - msgSize.height) / 2.0;
[text drawInRect:msgRect withAttributes:msgAttrs];
}
Also, I found a post describing a method on how to do this, but it did not work for me. The url to that is this: http://mattgemmell.com/2008/03/04/using-maattachedwindow-with-an-nsstatusitem/comment-page-1#comment-46501.
Thanks!
Use an NSImage and draw it where desired. For example:
NSString *name = clicked? #"icon-active" : #"icon";
NSImage *image = [NSImage imageNamed:name];
NSPoint p = [self bounds].origin;
[image drawAtPoint:p fromRect:NSZeroRect
operation:NSCompositeSourceOver fraction:1.0];
If this is for a status item and you just want an icon with no programmatic drawing, drop the view and set the status item's image and alternateImage. The former is what the status item uses normally; the status item switches to the alternate image (if it has one) when the user opens its menu.