How do I get a NSString's size as if it drew in an NSRect. The problem is when I try -[NSString sizeWithAttributes:], it returns an NSSize as if it had infinite width. I want to give a maximum width to the method. Is there any way of doing so? (BTW: Mac OS, not iPhone OS)
Thanks,
Alex
float heightForStringDrawing(NSString *myString, NSFont *myFont,
float myWidth)
{
NSTextStorage *textStorage = [[[NSTextStorage alloc] initWithString:myString] autorelease];
NSTextContainer *textContainer = [[[NSTextContainer alloc] initWithContainerSize:NSMakeSize(myWidth, FLT_MAX)] autorelease];
;
NSLayoutManager *layoutManager = [[[NSLayoutManager alloc] init] autorelease];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:myFont
range:NSMakeRange(0, [textStorage length])];
[textContainer setLineFragmentPadding:0.0];
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager
usedRectForTextContainer:textContainer].size.height;
}
It was in the docs after all. Thank you Joshua anyway!
I revised Alexandre Cassagne's answer for iOS with ARC enabled.
CGSize ACMStringSize(NSString *string, UIFont *font, CGSize size)
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:string];
[textStorage addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [textStorage length])];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
return [layoutManager usedRectForTextContainer:textContainer].size;
}
I believe your only option here is NSLayoutManager and asking for a union of the used rects for a given glyph range.
for OSX cocoa/Swift 4:
(using a custom NSview, to show also how to draw)
//
// CustomView.swift
// DrawMyString
//
// Created by ing.conti on 19/10/2018.
// Copyright © 2018 com.ingconti. All rights reserved.
//
import Cocoa
class CustomView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let s : NSString = "AA BB CC DD EE FF"
let W = 50
let size = NSSize(width: W, height: 100)
let boundingRectWithSize = s.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: [:])
let r = NSRect(origin: CGPoint(x: 20, y: 30), size: boundingRectWithSize.size)
s.draw(in: r, withAttributes: [:])
r.frame()
}
}
(we also draw border, to see it)
Related
I have multiline NSTexField, that I use to show static text, that may have different length on each showing of window. This NSTextField has a height constrain, that limits it to two lines.
I want to setup behaviour, that NSTextField will grow horizontally only just so much, that it will fill those two horizontal lines.
Now when I set up horizontal compression resistance higher, text field width grows so much to display all the text on one line. Is this possible to achieve only by using autolayout constrains or do I have to calculate nstextfield width somehow?
I tried overloading "layout" method in superview. But its not working as I expect.
- (void)layout
{
[self solveLayoutForView:self];
}
- (void)solveLayoutForView:(NSView*)view
{
for (NSView* subView in [view subviews])
{
if (subView.subviews.count)
[self solveLayoutForView:subView];
else
{
NSLayoutConstraint* height = [subView constraintForAttribute:NSLayoutAttributeHeight];
if (height && height.constant > 17 && [subView isKindOfClass:[NSTextField class]]) // seems like multiline nstextfield
{
NSTextField* textField = (NSTextField*)subView;
textField.preferredMaxLayoutWidth = 0;
[super layout];
textField.preferredMaxLayoutWidth = NSWidth([textField alignmentRectForFrame:textField.frame]);
[super layout];
}
}
}
}
I ended up calculating it by myself.
I overload setStringValue in subclass of NSTextField.
"PreferGrowingHorizontally" is a subclass property if I ever need to skip this calculation (e.g.: in case I would like it to grow horizontally)
- (void)setStringValue:(NSString *)stringValue
{
BOOL changed = ![self.stringValue isEqualToString:stringValue] ? YES : NO;
[super setStringValue:stringValue];
if (stringValue.length && changed)
{
if (self.frame.size.height > 17 && self.preferGrowingHorizontally)
[self compensateWidth];
}
}
- (void)compensateWidth
{
float requiredHeight = FLT_MAX;
int widthCompensation = 0;
while (requiredHeight > self.frame.size.height)
requiredHeight = [self heightForStringDrawing:self.stringValue Font:self.font Width:self.frame.size.width + widthCompensation++];
if (--widthCompensation)
{
NSLayoutConstraint* width = [self constraintForAttribute:NSLayoutAttributeWidth];
if (width)
width.constant = self.frame.size.width + widthCompensation;
NSLog(#"requiredHeight: %f width compensation: %d for textField: %#", requiredHeight, widthCompensation, self.stringValue);
}
}
- (float)heightForStringDrawing:(NSString*)myString Font:(NSFont*)myFont Width:(float)myWidth
{
NSTextStorage *textStorage = [[[NSTextStorage alloc] initWithString:myString] autorelease];
NSTextContainer *textContainer = [[[NSTextContainer alloc] initWithContainerSize:NSMakeSize(myWidth, FLT_MAX)] autorelease];
NSLayoutManager *layoutManager = [[[NSLayoutManager alloc] init] autorelease];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:myFont
range:NSMakeRange(0, [textStorage length])];
[textContainer setLineFragmentPadding:0.0];
textContainer.lineBreakMode = NSLineBreakByWordWrapping;
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager usedRectForTextContainer:textContainer].size.height;
}
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 must be doing something wrong here:
My Cocoa app has a scrollview around a custom view which in turn has a textview. I only expect to see one "This is a " string but there the extra one up in the corner.
I have reduced the code to something very minimal and still do not understand what my error is, so here I am fishing for a clue.
The view controller for the custom view follows, but for simplicity here is a link to the project.
#import "TTTSimpleCtrlView.h"
#interface TTTSimpleCtrlView ()
#property (strong,nonatomic) NSTextView *tv1;
#property (strong,nonatomic) NSTextStorage *ts;
#end
#implementation TTTSimpleCtrlView
- (void) awakeFromNib {
NSFont *font = [NSFont fontWithName:#"Courier New Bold" size:20.0f];
NSMutableParagraphStyle *styleModel = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[styleModel setLineHeightMultiple:1.0];
// [styleModel setLineSpacing:fontRect.size.height * 2];
NSDictionary *textAttrs = [NSDictionary dictionaryWithObjectsAndKeys: font, NSFontAttributeName,
[NSColor blackColor] ,NSForegroundColorAttributeName,
[NSColor whiteColor], NSBackgroundColorAttributeName,
styleModel, NSParagraphStyleAttributeName,
nil];
NSString *pilcrowStr = #"This is a test.";
NSAttributedString *s = [[NSAttributedString alloc] initWithString:pilcrowStr attributes:textAttrs];
NSRect rect = [s boundingRectWithSize:NSMakeSize(INFINITY,INFINITY)options:0];
NSLayoutManager *lm = [[NSLayoutManager alloc] init];
NSTextContainer *tc = [NSTextContainer new];
[tc setContainerSize:s.size];
[lm addTextContainer:tc];
_ts = [[NSTextStorage alloc] init];
[_ts setAttributedString:s];
[_ts addLayoutManager:lm];
[lm replaceTextStorage:_ts];
rect.origin.x = 10;
rect.origin.y = rect.size.height;
NSTextView *v = [[NSTextView alloc] initWithFrame:rect textContainer:tc];
[v setDrawsBackground:YES];
[self addSubview:v];
}
- (BOOL) isFlipped {
return YES;
}
- (void)drawRect:(NSRect)rect
{
NSLog(#"drawRect & %lu subviews",self.subviews.count);
for (NSTextView *v in self.subviews) {
if(CGRectIntersectsRect(v.frame, rect) || CGRectContainsRect(rect, v.frame)) {
[v drawRect:rect];
NSLog(#"frame = %#",NSStringFromRect(v.frame));
}
}
[super drawRect:rect];
}
You are calling:
[super drawRect:rect];
and you are drawing the text yourself in your draw function.
In effect you are drawing the text and cocoa is drawing the text for you as well.
So don't call super.
"Now Playing" is in One line in UIBarButtonItem. I have to put it in two lines, like "Now" is ay top and "Playing" is at bottom.I have written the following line of code:-
UIBarButtonItem *flipButton = [[UIBarButtonItem alloc]
initWithTitle:#"Now Playing"
style:UIBarButtonItemStyleBordered
target:self
action:#selector(flipView)];
self.navigationItem.rightBarButtonItem = flipButton;
So i want to pu line break in between "Now Playing". So please help me out.
Yes you can. It is fairly simple to do. Create a multiline button and use that. The "trick" is to set the titleLabel numberOfLines property so that it likes multilines.
UIButton* button = [UIButton buttonWithType:UIButtonTypeCustom];
button.titleLabel.numberOfLines = 0;
[button setTitle:NSLocalizedString(#"Now\nPlaying", nil) forState:UIControlStateNormal];
[button sizeToFit];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
When you specify a button type of custom it expects you to configure everything... selected state, etc. which is not shown here to keep the answer focused to the problem at hand.
- (void)createCustomRightBarButton_
{
UILabel * addCustomLabel = [[[UILabel alloc] initWithFrame:CGRectMake(0, 0, 64, 25)] autorelease];
addCustomLabel.text =#"Now\nPlaying";
addCustomLabel.textColor = [UIColor whiteColor];
addCustomLabel.font = [UIFont boldSystemFontOfSize:11];
addCustomLabel.numberOfLines = 2;
addCustomLabel.backgroundColor = [UIColor clearColor];
addCustomLabel.textAlignment = UITextAlignmentCenter;
CGSize size = addCustomLabel.bounds.size;
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
[addCustomLabel.layer renderInContext: context];
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage * img = [UIImage imageWithCGImage: imageRef];
CGImageRelease(imageRef);
CGContextRelease(context);
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithImage:img
style:UIBarButtonItemStyleBordered
target:self
action:#selector(flipView)]
autorelease];
}
You can host a UIButton as customView inside your bar button (either set the bar button item customView or you can drag one directly on top of your UIBarButtonItem), hook it up as an outlet, then do;
-(void) viewDidLoad
{
//...
self.customButton.titleLabel.numberOfLines = 2;
self.customButton.suggestionsButton.titleLabel.lineBreakMode = UILineBreakModeWordWrap;
self.customButton.suggestionsButton.titleLabel.textAlignment = UITextAlignmentCenter;
}
which in my case becomes
I create 2 PNG images by myself, and it looks good.
UIImage *img = [UIImage imageNamed:#"nowplaying.png"];
UIBarButtonItem *nowPlayingButtonItem = [[[UIBarButtonItem alloc] initWithImage:img style:UIBarButtonItemStyleBordered target:delegate action:#selector(presentNowPlayingMovie)] autorelease];
I am trying to set an attributed string within NSTextView. I want to increase its height based on its content, initially it is set to some default value.
So I tried this method:
I set content in NSTextView. When we set some content in NSTextView its size automatically increases. So I increased height of its super view that is NSScrollView to its height, but NSScrollView is not getting completely resized, it is showing scroller on right.
float xCoordinate = 15.0;
[xContentViewScroller setFrame:NSMakeRect(xCoordinate, 0.0, 560.0, 10.0)];
[[xContentView textStorage] setAttributedString:xContents];
float xContentViewScrollerHeight = [xfContentView frame].size.height + 2;
[xContentViewScroller setFrame:NSMakeRect(xCoordinate, 0.0, 560.0, xContentViewScrollerHeight)];
Can anyone suggest me some way or method to resolve this issue. By doing google search I found that in UITextView there is contentSize method which can get size of its content, I tried to find similar method in NSTextView but could not get any success :(
Based on #Peter Hosey's answer, here is an extension to NSTextView in Swift 4.2:
extension NSTextView {
var contentSize: CGSize {
get {
guard let layoutManager = layoutManager, let textContainer = textContainer else {
print("textView no layoutManager or textContainer")
return .zero
}
layoutManager.ensureLayout(for: textContainer)
return layoutManager.usedRect(for: textContainer).size
}
}
}
NSTextView *textView = [[NSTextView alloc] init];
textView.font = [NSFont systemFontOfSize:[NSFont systemFontSize]];
textView.string = #"Lorem ipsum";
[textView.layoutManager ensureLayoutForTextContainer:textView.textContainer];
textView.frame = [textView.layoutManager usedRectForTextContainer:textView.textContainer];
+ (float)heightForString:(NSString *)myString font:(NSFont *)myFont andWidth:(float)myWidth andPadding:(float)padding {
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:myString];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(myWidth, FLT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[textStorage addAttribute:NSFontAttributeName value:myFont
range:NSMakeRange(0, textStorage.length)];
textContainer.lineFragmentPadding = padding;
(void) [layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager usedRectForTextContainer:textContainer].size.height;
}
I did the function using this reference: Documentation
Example:
float width = textView.frame.size.width - 2 * textView.textContainerInset.width;
float proposedHeight = [Utils heightForString:textView.string font:textView.font andWidth:width
andPadding:textView.textContainer.lineFragmentPadding];