Sizing a UILabel to fit? - cocoa-touch

How would one modify the following snippet (in a tableView:cellForRowAtIndexPath: UITableViewController method) from the "09a - PrefsTable" recipe from Chapter 6 of The iPhone Developer's Cookbook:
if (row == 1) {
// Create a big word-wrapped UILabel
cell = [tableView dequeueReusableCellWithIdentifier:#"libertyCell"];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:#"libertyCell"] autorelease];
[cell addSubview:[[UILabel alloc] initWithFrame:CGRectMake(20.0f, 10.0f, 280.0f, 330.0f)]];
}
UILabel *sv = [[cell subviews] lastObject];
sv.text =  #"When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.";
sv.textAlignment = UITextAlignmentCenter;
sv.lineBreakMode = UILineBreakModeWordWrap;
sv.numberOfLines = 9999;
return cell;
}
...to size the "sv" UILabel subview and the "cell" UITableViewCell to be sized just big enough to fit the text (and work with more or less text, and other types of text alignment)?  I looked at the UILabel textRectForBounds:limitedToNumberOfLines: method, but the documentation states that it should not be called directly (and should only be overridden).  I experimented with the UIView sizeToFit method, without success.
Update: I asked a new question about my problem with the NSString -sizeWithFont:forWidth:lineBreakMode: method.

I had to do this enough that I extended UILabel to do it for me:
#interface UILabel (BPExtensions)
- (void)sizeToFitFixedWidth:(CGFloat)fixedWidth;
#end
#implementation UILabel (BPExtensions)
- (void)sizeToFitFixedWidth:(CGFloat)fixedWidth
{
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, fixedWidth, 0);
self.lineBreakMode = NSLineBreakByWordWrapping;
self.numberOfLines = 0;
[self sizeToFit];
}
#end
then to have a label to have a variable multiline height but a fixed width just:
[myLabel sizeToFitFixedWidth:kSomeFixedWidth];

You should use NSString's -sizeWithFont:forWidth:lineBreakMode: method to retrieve the associated sizing metrics for your label.

Also, change the numberOfLines property to 0 if you're going to use that code.

NSString's -sizeWithFont:forWidth:lineBreakMode: does not actually perform the word wrap. Instead, use -sizeWithFont:constrainedToSize:lineBreakMode: to get an accurate width AND height value for the string.

Try this:
sv.text = #"When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.";
sv.textAlignment = UITextAlignmentCenter;
sv.lineBreakMode = UILineBreakModeWordWrap;
sv.numberOfLines = 0;
[sv sizeToFit];

Also, you will need to implement the UITableViewDelegate method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
And have it return a total cell height adjusted for the resized text field.
One other note - Size to Fit should actually work, if you have number of lines set to 0 as previously mentioned. It would give you back a size with the height increased to accomidate the word-wrapped text set in the label and the width set to whatever the original label width had.
That will not help you though as you need to get the size in heightForRow before the cell is obtained, so you are better off calculating the height needed (and very probably caching that calculation so as not to slow down table rendering)

Here's a bit of code i use:
CGSize textSize = [myLabel.text sizeWithFont:myLabel.font];

I had similar problem, I had a UITableViewCell that was designed in StoryBoards as a static cell. I used [super tableView:cellForRowAtIndexPath:] to get it. So I wanted to resize the UILabel "detailTextLabel" so it fits the text I set to it. The style was "Right Detail".
I just set the text in my tableView:cellForRowAtIndexPath:. And than in tableView:heightForRowAtIndexPath: I returned
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
return cell.detailTextLabel.frame.size.height
I had long string. And finally had a wide Cell with 4 lines of text in label.

I had similar problem.
I solved this.
In cellForRowAtIndexPath method set font size to whatever you want.
cell.textLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.textLabel.numberOfLines = 0;
[cell.textLabel setFont:[UIFont systemFontOfSize:14.0]];
[cell.textLabel sizeToFit];
And in heightForRowAtIndexPath method increase font size.
CGFloat height;
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
NSString *text = cell.detailTextLabel.text;
CGSize constraint = CGSizeMake(320, 20000.0f);
CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:20.0] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
CGFloat calHeight = MAX(size.height, 44.0f);
height = calHeight + (CELL_CONTENT_MARGIN * 2);
return height;

Related

sizeWithFont: and sizeWithAttributes: don't work for a long single line

I'm trying to set a dynamic height for cells in my table, height should be based on a text length and a max width.
The problem appears when this text comes in a single line, without line separators. Doesn't matter how large the text is, if there are no line separators it detects that text fits in a single line so my cell height doesn't increase.
Am I doing something wrong? How can I achieve it? Thanks.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat cellheight = 35.0f; // BASE
NSString *text = #"...";
if (text) {
UIFont *font = (indexPath.row == 0) ? [UIFont systemFontOfSize:14] : [UIFont systemFontOfSize:12];
CGSize constraintSize = CGSizeMake(self.tableView.frame.size.width, CGFLOAT_MAX);
if (IS_EARLIER_THAN_IOS7) {
CGSize size = [text sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:NSLineBreakByCharWrapping];
cellheight += size.height;
} else {
CGSize size = [text sizeWithAttributes:#{NSFontAttributeName: [UIFont systemFontOfSize:12.0f]}];
CGSize adjustedSize = CGSizeMake(ceilf(size.width), ceilf(size.height));
cellheight += adjustedSize.height;
}
return (indexPath.row == 0) ? cellheight + 40.0f : cellheight;
}
}
- (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(NSLineBreakMode)lineBreakMode NS_DEPRECATED_IOS(2_0, 7_0, "Use -boundingRectWithSize:options:attributes:context:") __TVOS_PROHIBITED; // NSTextAlignment is not needed to determine size
You should use "boundingRectWithSize:options:attributes:context:" not "sizeWithAttributes:".
This is a sample
CGSize size = [text boundingRectWithSize:CGSizeMake(_myTableView.frame.size.width, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName:[UIFont systemFontOfSize:14]} context:nil].size;
There is a way more easy way to do that:
First set the text to the UILabel, and set all the required font, size. etc. Then call sizeThatFits method on the label.
CGSize sizze =[itemLabel sizeThatFits:CGSizeMake(itemNameLabelWidth, CGFLOAT_MAX)];
Also dont forget to set numberOfLines and lineBreakMode before calling sizeThatFits:
itemLabel.numberOfLines=0;
itemLabel.lineBreakMode=NSLineBreakByWordWrapping;
Note 1 : calling sizeThatFits does not set the new frame to the UILabel, it just calculates and returns the new frame. You have to then set the frame to the label by adding x and y origin values. So that becomes :
CGSize sizze =[itemLabel sizeThatFits:CGSizeMake(itemNameLabelWidth, CGFLOAT_MAX)];
CGRect namelabelFrame = itemLabel.frame;
namelabelFrame.size = sizze;
itemLabel.frame = namelabelFrame;
Note 2 : This code is okay in cellForRowAtIndexPath:, but when calculating the height inside heightForRowAtIndexPath: you may want to optimize this code a little bit. As you don't have the cell to work with you might initialize a UILabel object and perform this code on it to estimate the height. But having UIView initializations inside heightForRowAtIndexPath: is not a good idea as they can significantly affect performance while scrolling.
So what you do is have an already initialised (and all formatting applied) UILabel as a class variable and reuse that for height calculation.

UITableViewCell Auto Resizing Multiple UILabels

We are currently resizing the height of a UILabel in a UITableViewCell. Now the client wants to have both labels contained within the UITableViewCell to be resized based on the content. Researching this we see solutions everywhere for resizing a single label but nothing about multiple labels. Can somebody point us to a good resource for this issue?
your research for resizing label is correct , you need write the logic for resizing on one label only. if you see in
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
its return type uitableviewcell. it always return the same configuration for the cell(unless you are creating static cell for each row).
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
this method dequeue the cell & reuse it . so you need to right the logic for resizing the label where you are creating your cell. or its better to right the resize logic in custom cell class. i am adding the code , which help me in this problem.
CGSize maximumLabelSize = CGSizeMake(270, FLT_MAX);
CGSize expectedLabelSize = [myString sizeWithFont:[UIFont fontWithName:#"Helvetica" size:16.0f] constrainedToSize:maximumLabelSize lineBreakMode:NSLineBreakByWordWrapping];
NSLog(#"Expected Size of label based on string--%#",NSStringFromCGSize(expectedLabelSize) );
//adjust the label the the new height.
CGRect newFrame = myLabel.frame;//get initial frame of your label
newFrame.size.height = expectedLabelSize.height;
myLabel.frame = newFrame;
NSLog(#"new frame of label--%#",NSStringFromCGRect(newFrame));
[myLabel setText:attString];

Dynamic table cell height with Autolayout iOS 6 +

I have UITableviewCell subclass. In that cell, I have 2 labels (lblComment and lblDateTimeStampe) and one view to show rating stars.
I want dynamic height of lblComment to fit all the text. It should expand & shrink in height depending on the length of comment.
I have implemented this before but WITHOUT AutoLayout like below
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *label = self.userComment.commentText;
CGSize stringSize = [label sizeWithFont:[UIFont boldSystemFontOfSize:15]
constrainedToSize:CGSizeMake(320, 9999)
lineBreakMode:UILineBreakModeWordWrap];
return stringSize.height+10;
}
Now I am using AutoLayout feature.
How can I achieve this using Autolayout?
Any kind of help is appreciated. Thanks
Unfortunately Auto Layout won't help you with tableView:heightForRowAtIndexPath. You still have to implement that method.
You can use UIView's systemLayoutSizeFittingSize: method but that means you'd have to instantiate and configure a table view cell which can be quite costly. You could keep one off-screen though and reuse it for calculations. But at this point, you don't really save much in terms of development effort, so doing the calculations as you did before manually is probably the best/fastest way to do this.
You can use the freely available Sensible TableView framework. The framework automatically resizes the cells as their content grows. It also does that dynamically if the table view is already displayed.
I have implemented a solution to the same problem with using autolayout and it works.
First, you need to define heightConstraint for lblComment.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *font = [UIFont fontWithName:#"YourFontName" size:YourFontSize] ;
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setFont:font];
[calculationView setTextAlignment:NSTextAlignmentLeft];
[calculationView setText:lblComment.text];
int width = 0;
if(self.appDelegate.isDeviceiPhone)
width = 284;
else
width = 720;
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//initialize the cell..
UIFont *font = [UIFont fontWithName:#"YourFontName" size:YourFontSize];
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setFont:font];
[calculationView setTextAlignment:NSTextAlignmentLeft];
[calculationView setText:cell.lblComment.text];
int width = 0;
if(self.appDelegate.isDeviceiPhone)
width = 284;
else
width = 720;
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
cell.lblDetailHeightConstraint.constant = size.height;
// the other stuff...
}
Hope this helps.
If you setup the constraints correctly in IB this should work. You should not have to add the elements programmatically although as you said that will work as well.
Assuming that you have label1(variable height), label2, and view1 in the tableviewcell in that order you should:
Set fixed height constraints on label2 and view1
Pin the bottom of view1 to the bottom of the cell
Pin the vertical spacing of view1 and label2
Pin the vertical spacing of label2 and label1
Pin the top spacing of label1 to the top of the cell
Just make sure that you do NOT have a height constraint on label1, if you do it should only be greater than or equal to. With a configuration like this, you can continue using heightForRowAtIndexPath and label1 will expand and contract vertically based on the height of the cell.

uilabel tail truncation

Im working on an ios app using objective c and i have an issue with uilabel that i could use some help with. Basically i have a label that can change size to fit the text that it will display but it has a max height that it can possible be. the label itself has a fixed width at all times. i have turned on UILineBreakModeWordWrap and UILineBreakModeTailTruncation to make the text fit and truncate but this causes the text to truncate the tail too early when it has only 1 word left to place. rather then moving it onto the next line when there is still room it just truncates it.
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, fixedWidth, 0);
self.lineBreakMode = UILineBreakModeWordWrap | UILineBreakModeTailTruncation;
self.numberOfLines = 0;
[self sizeToFit];
is there anyway of finding when the uilabel is actually truncating the text so i can then check the label height and add to it if there is still room ? I tried always adding an extra line to the height when there is room and this avoids the early truncation but then im left with inconsistent sizing of the over all label. any ideas on this would be great thanks
lineBreakMode is a switch. It can be either (for iOS6+) NSLineBreakByWordWrapping or NSLineBreakByTruncatingTail but not both.
But, to answer your question, you can find the size of some text using the class extensions in NSString+UIKit. Having found the size you could update the frame of the UILabel appropriately.
Using this method:
How to find UILabel's number of Lines
You could set the label to the max height, find out how tall the text is in that label and shrink it as necessary.
I have written a category for working with UILabel's truncation. Works on iOS 7 and later. Hope it helps !
#implementation UILabel (Truncation)
- (NSRange)truncatedRange
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
textContainer.lineFragmentPadding = 0;
[layoutManager addTextContainer:textContainer];
NSRange truncatedrange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:0];
return truncatedrange;
}
- (BOOL)isTruncated
{
return [self truncatedRange].location != NSNotFound;
}
- (NSString *)truncatedText
{
NSRange truncatedrange = [self truncatedRange];
if (truncatedrange.location != NSNotFound)
{
return [self.text substringWithRange:truncatedrange];
}
return nil;
}
#end
I just came to a similar problem, and solved it with a very simple solution (tested on ios 8.4, xcode 7).
In IB (I use autolayout with some constraints):
set UIlabel numberOfLine = 1;
LineBrakeMode = TruncateTail
In Code:
label.numberOfLines = 2; // I can only display 2 row.
// I supposed you should know how many rows you can displayed.
label.preferredMaxLayoutWidth = label.frame.size.width;
[label sizeToFit];
Tadaa. That's it. Note that this only work with UILabel. With UIButton, it may not work (Haven't tested).

View-based NSTableView with rows that have dynamic heights

I have an application with a view-based NSTableView in it. Inside this table view, I have rows that have cells that have content consisting of a multi-row NSTextField with word-wrap enabled. Depending on the textual content of the NSTextField, the size of the rows needed to display the cell will vary.
I know that I can implement the NSTableViewDelegate method -tableView:heightOfRow: to return the height, but the height will be determined based on the word wrapping used on the NSTextField. The word wrapping of the NSTextField is similarly based on how wide the NSTextField is… which is determined by the width of the NSTableView.
Soooo… I guess my question is… what is a good design pattern for this? It seems like everything I try winds up being a convoluted mess. Since the TableView requires knowledge of the height of the cells to lay them out... and the NSTextField needs knowledge of it's layout to determine the word wrap… and the cell needs knowledge of the word wrap to determine it's height… it's a circular mess… and it's driving me insane.
Suggestions?
If it matters, the end result will also have editable NSTextFields that will resize to adjust to the text within them. I already have this working on the view level, but the tableview does not yet adjust the heights of the cells. I figure once I get the height issue worked out, I'll use the -noteHeightOfRowsWithIndexesChanged method to inform the table view the height changed… but it's still then going to ask the delegate for the height… hence, my quandry.
This is a chicken and the egg problem. The table needs to know the row height because that determines where a given view will lie. But you want a view to already be around so you can use it to figure out the row height. So, which comes first?
The answer is to keep an extra NSTableCellView (or whatever view you are using as your "cell view") around just for measuring the height of the view. In the tableView:heightOfRow: delegate method, access your model for 'row' and set the objectValue on NSTableCellView. Then set the view's width to be your table's width, and (however you want to do it) figure out the required height for that view. Return that value.
Don't call noteHeightOfRowsWithIndexesChanged: from in the delegate method tableView:heightOfRow: or viewForTableColumn:row: ! That is bad, and will cause mega-trouble.
To dynamically update the height, then what you should do is respond to the text changing (via the target/action) and recalculate your computed height of that view. Now, don't dynamically change the NSTableCellView's height (or whatever view you are using as your "cell view"). The table must control that view's frame, and you will be fighting the tableview if you try to set it. Instead, in your target/action for the text field where you computed the height, call noteHeightOfRowsWithIndexesChanged:, which will let the table resize that individual row. Assuming you have your autoresizing mask setup right on subviews (i.e.: subviews of the NSTableCellView), things should resize fine! If not, first work on the resizing mask of the subviews to get things right with variable row heights.
Don't forget that noteHeightOfRowsWithIndexesChanged: animates by default. To make it not animate:
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0];
[tableView noteHeightOfRowsWithIndexesChanged:indexSet];
[NSAnimationContext endGrouping];
PS: I respond more to questions posted on the Apple Dev Forums than stack overflow.
PSS: I wrote the view based NSTableView
This got a lot easier in macOS 10.13 with .usesAutomaticRowHeights. The details are here: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (In the section titled "NSTableView Automatic Row Heights").
Basically you just select your NSTableView or NSOutlineView in the storyboard editor and select this option in the Size Inspector:
Then you set the stuff in your NSTableCellView to have top and bottom constraints to the cell and your cell will resize to fit automatically. No code required!
Your app will ignore any heights specified in heightOfRow (NSTableView) and heightOfRowByItem (NSOutlineView). You can see what heights are getting calculated for your auto layout rows with this method:
func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) {
print(rowView.fittingSize.height)
}
Based on Corbin's answer (btw thanks shedding some light on this):
Swift 3, View-Based NSTableView with Auto-Layout for macOS 10.11 (and above)
My setup: I have a NSTableCellView that is laid out using Auto-Layout. It contains (besides other elements) a multi-line NSTextField that can have up to 2 rows. Therefore, the height of the whole cell view depends on the height of this text field.
I update tell the table view to update the height on two occasions:
1) When the table view resizes:
func tableViewColumnDidResize(_ notification: Notification) {
let allIndexes = IndexSet(integersIn: 0..<tableView.numberOfRows)
tableView.noteHeightOfRows(withIndexesChanged: allIndexes)
}
2) When the data model object changes:
tableView.noteHeightOfRows(withIndexesChanged: changedIndexes)
This will cause the table view to ask it's delegate for the new row height.
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
// Get data object for this row
let entity = dataChangesController.entities[row]
// Receive the appropriate cell identifier for your model object
let cellViewIdentifier = tableCellViewIdentifier(for: entity)
// We use an implicitly unwrapped optional to crash if we can't create a new cell view
var cellView: NSTableCellView!
// Check if we already have a cell view for this identifier
if let savedView = savedTableCellViews[cellViewIdentifier] {
cellView = savedView
}
// If not, create and cache one
else if let view = tableView.make(withIdentifier: cellViewIdentifier, owner: nil) as? NSTableCellView {
savedTableCellViews[cellViewIdentifier] = view
cellView = view
}
// Set data object
if let entityHandler = cellView as? DataEntityHandler {
entityHandler.update(with: entity)
}
// Layout
cellView.bounds.size.width = tableView.bounds.size.width
cellView.needsLayout = true
cellView.layoutSubtreeIfNeeded()
let height = cellView.fittingSize.height
// Make sure we return at least the table view height
return height > tableView.rowHeight ? height : tableView.rowHeight
}
First, we need to get our model object for the row (entity) and the appropriate cell view identifier. We then check if we have already created a view for this identifier. To do that we have to maintain a list with cell views for each identifier:
// We need to keep one cell view (per identifier) around
fileprivate var savedTableCellViews = [String : NSTableCellView]()
If none is saved, we need to created (and cache) a new one. We update the cell view with our model object and tell it to re-layout everything based on the current table view width. The fittingSize height can then be used as the new height.
For anyone wanting more code, here is the full solution I used. Thanks corbin dunn for pointing me in the right direction.
I needed to set the height mostly in relation to how high a NSTextView in my NSTableViewCell was.
In my subclass of NSViewController I temporary create a new cell by calling outlineView:viewForTableColumn:item:
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
NSTableColumn *tabCol = [[outlineView tableColumns] objectAtIndex:0];
IBAnnotationTableViewCell *tableViewCell = (IBAnnotationTableViewCell*)[self outlineView:outlineView viewForTableColumn:tabCol item:item];
float height = [tableViewCell getHeightOfCell];
return height;
}
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
IBAnnotationTableViewCell *tableViewCell = [outlineView makeViewWithIdentifier:#"AnnotationTableViewCell" owner:self];
PDFAnnotation *annotation = (PDFAnnotation *)item;
[tableViewCell setupWithPDFAnnotation:annotation];
return tableViewCell;
}
In my IBAnnotationTableViewCell which is the controller for my cell (subclass of NSTableCellView) I have a setup method
-(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation;
which sets up all outlets and sets the text from my PDFAnnotations. Now I can "easily" calcutate the height using:
-(float)getHeightOfCell
{
return [self getHeightOfContentTextView] + 60;
}
-(float)getHeightOfContentTextView
{
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[self.contentTextView font],NSFontAttributeName,nil];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:[self.contentTextView string] attributes:attributes];
CGFloat height = [self heightForWidth: [self.contentTextView frame].size.width forString:attributedString];
return height;
}
.
- (NSSize)sizeForWidth:(float)width height:(float)height forString:(NSAttributedString*)string
{
NSInteger gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
NSSize answer = NSZeroSize ;
if ([string length] > 0) {
// Checking for empty string is necessary since Layout Manager will give the nominal
// height of one line if length is 0. Our API specifies 0.0 for an empty string.
NSSize size = NSMakeSize(width, height) ;
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:size] ;
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:string] ;
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init] ;
[layoutManager addTextContainer:textContainer] ;
[textStorage addLayoutManager:layoutManager] ;
[layoutManager setHyphenationFactor:0.0] ;
if (gNSStringGeometricsTypesetterBehavior != NSTypesetterLatestBehavior) {
[layoutManager setTypesetterBehavior:gNSStringGeometricsTypesetterBehavior] ;
}
// NSLayoutManager is lazy, so we need the following kludge to force layout:
[layoutManager glyphRangeForTextContainer:textContainer] ;
answer = [layoutManager usedRectForTextContainer:textContainer].size ;
// Adjust if there is extra height for the cursor
NSSize extraLineSize = [layoutManager extraLineFragmentRect].size ;
if (extraLineSize.height > 0) {
answer.height -= extraLineSize.height ;
}
// In case we changed it above, set typesetterBehavior back
// to the default value.
gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ;
}
return answer ;
}
.
- (float)heightForWidth:(float)width forString:(NSAttributedString*)string
{
return [self sizeForWidth:width height:FLT_MAX forString:string].height ;
}
I was looking for a solution for quite some time and came up with the following one, which works great in my case:
- (double)tableView:(NSTableView *)tableView heightOfRow:(long)row
{
if (tableView == self.tableViewTodo)
{
CKRecord *record = [self.arrayTodoItemsFiltered objectAtIndex:row];
NSString *text = record[#"title"];
double someWidth = self.tableViewTodo.frame.size.width;
NSFont *font = [NSFont fontWithName:#"Palatino-Roman" size:13.0];
NSDictionary *attrsDictionary =
[NSDictionary dictionaryWithObject:font
forKey:NSFontAttributeName];
NSAttributedString *attrString =
[[NSAttributedString alloc] initWithString:text
attributes:attrsDictionary];
NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
[[tv textStorage] setAttributedString:attrString];
[tv setHorizontallyResizable:NO];
[tv sizeToFit];
double height = tv.frame.size.height + 20;
return height;
}
else
{
return 18;
}
}
Since I use custom NSTableCellView and I have access to the NSTextField my solution was to add a method on NSTextField.
#implementation NSTextField (IDDAppKit)
- (CGFloat)heightForWidth:(CGFloat)width {
CGSize size = NSMakeSize(width, 0);
NSFont* font = self.font;
NSDictionary* attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSRect bounds = [self.stringValue boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:attributesDictionary];
return bounds.size.height;
}
#end
Have you had a look at RowResizableViews? It is quite old and I haven't tested it but it may nevertheless work.
Here's what I have done to fix it:
Source: Look into XCode documentation, under "row height nstableview". You'll find a sample source code named "TableViewVariableRowHeights/TableViewVariableRowHeightsAppDelegate.m"
(Note: I'm looking at column 1 in table view, you'll have to tweak to look elsewhere)
in Delegate.h
IBOutlet NSTableView *ideaTableView;
in Delegate.m
table view delegates control of row height
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row {
// Grab the fully prepared cell with our content filled in. Note that in IB the cell's Layout is set to Wraps.
NSCell *cell = [ideaTableView preparedCellAtColumn:1 row:row];
// See how tall it naturally would want to be if given a restricted with, but unbound height
CGFloat theWidth = [[[ideaTableView tableColumns] objectAtIndex:1] width];
NSRect constrainedBounds = NSMakeRect(0, 0, theWidth, CGFLOAT_MAX);
NSSize naturalSize = [cell cellSizeForBounds:constrainedBounds];
// compute and return row height
CGFloat result;
// Make sure we have a minimum height -- use the table's set height as the minimum.
if (naturalSize.height > [ideaTableView rowHeight]) {
result = naturalSize.height;
} else {
result = [ideaTableView rowHeight];
}
return result;
}
you also need this to effect the new row height (delegated method)
- (void)controlTextDidEndEditing:(NSNotification *)aNotification
{
[ideaTableView reloadData];
}
I hope this helps.
Final note: this does not support changing column width.
Here is a solution based of JanApotheker's answer, modified as cellView.fittingSize.height was not returning the correct height for me. In my case I am using the standard NSTableCellView, an NSAttributedString for the cell's textField text, and a single column table with constraints for the cell's textField set in IB.
In my view controller, I declare:
var tableViewCellForSizing: NSTableCellView?
In viewDidLoad():
tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView
Finally, for the tableView delegate method:
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let tableCellView = tableViewCellForSizing else { return minimumCellHeight }
tableCellView.textField?.attributedStringValue = attributedString[row]
if let height = tableCellView.textField?.fittingSize.height, height > 0 {
return height
}
return minimumCellHeight
}
mimimumCellHeight is a constant set to 30, for backup, but never actually used. attributedStrings is my model array of NSAttributedString.
This works perfectly for my needs. Thanks for all the previous answers, which pointed me in the right direction for this pesky problem.
This sounds a lot like something I had to do previously. I wish I could tell you that I came up with a simple, elegant solution but, alas, I did not. Not for lack of trying though. As you have already noticed the need of UITableView to know the height prior to the cells being built really make it all seem quite circular.
My best solution was to push logic to the cell, because at least I could isolate what class needed to understand how the cells were laid out. A method like
+ (CGFloat) heightForStory:(Story*) story
would be able to determine how tall the cell had to be. Of course that involved measuring text, etc. In some cases I devised ways to cache information gained during this method that could then be used when the cell was created. That was the best I came up with. It is an infuriating problem though as it seems there should be a better answer.