UICollectionView column grid gaps - objective-c

I am using UICollectionView flow layout in my iPad application. I have cells with different width and heights. When I used subclass of UICollectionViewFlowLayout as layout for collection view, it shows me gaps between cells.
If there is large cell in column, it makes grid and small cells of that column get render centered of that large cell in column. Is there any better way to tight cell wrapping?
I am using horizontal scrolling. I tried using layoutAttributesForElementsInRect: method, but didn't get succeed into that.
Please let me know if anyone has done same kind of thing by using layoutAttributesForElementsInRect: method or by other way...
Please find attached image. I need same kind of solution
Thanks in advance...

Use like this
UICollectionViewFlowLayout *layout=[[UICollectionViewFlowLayout alloc] init];
layout.minimumInteritemSpacing = 0;
layout.minimumLineSpacing = 2;

I subclassed UICollectionViewFlowLayout and in both my layoutAttributesForItemAtIndexPath: and layoutAttributesForElementsInRect: methods I have one method called
setLineAttributes:(UICollectionViewLayoutAttributes *)attributes visibleRect:(CGRect)visibleRect:
which contains:
if (attributes.indexPath.item == 0 || attributes.indexPath.item == 1 || attributes.indexPath.item == 2) {
CGRect f = attributes.frame;
f.origin.x = 0;
attributes.frame = f;
} else {
NSIndexPath *ipPrev = [NSIndexPath indexPathForItem:attributes.indexPath.item - 3 inSection:attributes.indexPath.section];
CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;
if (attributes.frame.origin.x <= rightPrev)
return;
CGRect f = attributes.frame;
f.origin.x = rightPrev;
attributes.frame = f;
}

Related

Adjust UICollectionViewFlowLayout When UICollectionView Frame Changes

I have a UICollectionView that I created programmatically, along with its UICollectionViewFlowLayout. The code for this looks like:
- (UICollectionView *)createCollectionToDisplayContent:(NSArray *)array ViewWithCellIdentifier:(NSString *)cellIdentifier ofWidth:(CGFloat)width withHeight:(CGFloat)height forEmailView:(EmailView *)emailView
{
CGFloat minInterItemSpacing;
if (!self.orientationIsLandscape) minInterItemSpacing = 9.0f;
else minInterItemSpacing = 20.0f;
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake(220.0f, 45.0f);
layout.sectionInset = UIEdgeInsetsMake(5.0f, 0.0f, 5.0f, 0.0f);
layout.minimumLineSpacing = 10.0f;
layout.minimumInteritemSpacing = minInterItemSpacing;
//get pointer to layout so I can change it later
emailView.personLabelsLayout = layout;
UICollectionView *collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(40, 4, width, height) collectionViewLayout:layout];
collectionView.dataSource = emailView;
collectionView.delegate = emailView;
collectionView.scrollEnabled = NO;
collectionView.userInteractionEnabled = YES;
collectionView.backgroundColor = [UIColor clearColor];
[collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:cellIdentifier];
collectionView.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin);
[collectionView reloadData];
return collectionView;
}
This code works fine to initially display the collection view. My issue is in trying to change the flow layout when the device is rotated. I handle the change in the collection view frame using struts and springs (see above), and in the willAnimateRotationToInterfaceOrientation method, I adjust the minimumInterItemSpacing property of the flow layout, and finally call reload data on the collection view. Here is the code for that:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
CGFloat minInterItemSpacing;
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || toInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
minInterItemSpacing = 20.0f;
} else {
minInterItemSpacing = 9.0f;
}
self.visibleEmailView = //...more code
self.visibleEmailView.personLabelsLayout.minimumInteritemSpacing = minInterItemSpacing;
[self.visibleEmailView.personLabelsCollectionView reloadData];
//other stuff...
}
Ultimately, the flow layout should adjust dramatically when the view is rotated: in terms of the minimumInterItemSpacing but also in terms of the number of columns it displays. However, the layout is not adjusting as I would expect. I am having trouble figuring out what exactly is even happening. It looks like the minimumInterItemSpacing is not being reset on rotation, and it looks like for each cell, the same cell is being drawn over itself multiple times. Also, I'm getting crashes that say:
"the collection view's data source did not return a valid cell from -collectionView:cellForItemAtIndexPath: for index path <NSIndexPath 0x9be47e0> 2 indexes [0, 6]"
In trying to begin to narrow down what is going on here, I am wondering if anyone can tell me if my approach is correct, and/or if there is an obvious error in my code that might be causing some or all of this funky behavior.
Are you implementing anything like this?
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
]

Automatically adjust height of a NSTableView

I have asked this question once before, but I'm just not very satisfied with the solution.
Automatically adjust size of NSTableView
I want to display a NSTableView in a NSPopover, or in a NSWindow.
Now, the window's size should adjust with the table view.
Just like Xcode does it:
This is fairly simple with Auto Layout, you can just pin the inner view to the super view.
My problem is, that I can't figure out the optimal height of the table view.
The following code enumerates all available rows, but it doesn't return the correct value, because the table view has other elements like separators, and the table head.
- (CGFloat)heightOfAllRows:(NSTableView *)tableView {
CGFloat __block height;
[tableView enumerateAvailableRowViewsUsingBlock:^(NSTableRowView *rowView, NSInteger row) {
// tried it with this one
height += rowView.frame.size.height;
// and this one
// height += [self tableView:nil heightOfRow:row];
}];
return height;
}
1. Question
How can I fix this? How can I correctly calculate the required height of the table view.
2. Question
Where should I run this code?
I don't want to implement this in a controller, because it's definitely something that the table view should handle itself.
And I didn't even find any helpful delegate methods.
So I figured best would be if you could subclass NSTableView.
So my question 2, where to implement it?
Motivation
Definitely worth a bounty
This answer is for Swift 4, targeting macOS 10.10 and later:
1. Answer
You can use the table view's fittingSize to calculate the size of your popover.
tableView.needsLayout = true
tableView.layoutSubtreeIfNeeded()
let height = tableView.fittingSize.height
2. Answer
I understand your desire to move that code out of the view controller but since the table view itself knows nothing about the number of items (only through delegation) or model changes, I would put that in the view controller. Since macOS 10.10, you can use preferredContentSize on your NSViewController inside a popover to set the size.
func updatePreferredContentSize() {
tableView.needsLayout = true
tableView.layoutSubtreeIfNeeded()
let height = tableView.fittingSize.height
let width: CGFloat = 320
preferredContentSize = CGSize(width: width, height: height)
}
In my example, I'm using a fixed width but you could also use the calculated one (haven't tested it yet).
You would want to call the update method whenever your data source changes and/or when you're about to display the popover.
I hope this solves your problem!
You can query the frame of the last row to get the table view's height:
- (CGFloat)heightOfTableView:(NSTableView *)tableView
{
NSInteger rows = [self numberOfRowsInTableView:tableView];
if ( rows == 0 ) {
return 0;
} else {
return NSMaxY( [tableView rectOfRow:rows - 1] );
}
}
This assumes an enclosing scroll view with no borders!
You can query the tableView.enclosingScrollView.borderType to check whether the scroll view is bordered or not. If it is, the border width needs to be added to the result (two times; bottom and top). Unfortunately, I don't know of the top of my head how to get the border width.
The advantage of querying rectOfRow: is that it works in the same runloop iteration as a [tableView reloadData];. In my experience, querying the table view's frame does not work reliably when you do a reloadData first (you'll get the previous height).
Interface Builder in Xcode automatically puts the NSTableView in an NSScrollView. The NSScrollView is where the headers are actually located. Create a NSScrollView as your base view in the window and add the NSTableView to it:
NSScrollView * scrollView = [[NSScrollView alloc]init];
[scrollView setHasVerticalScroller:YES];
[scrollView setHasHorizontalScroller:YES];
[scrollView setAutohidesScrollers:YES];
[scrollView setBorderType:NSBezelBorder];
[scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];
NSTableView * table = [[NSTableView alloc] init];
[table setDataSource:self];
[table setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle];
[scrollView setDocumentView:table];
//SetWindow's main view to scrollView
Now you can interrogate the scrollView's contentView to find the size of the NSScrollView size
NSRect rectOfFullTable = [[scrollView contentView] documentRect];
Because the NSTableView is inside an NSScrollView, the NSTableView will have a headerView which you can use to find the size of your headers.
You could subclass NSScrollView to update it's superview when the table size changes (headers + rows) by overriding the reflectScrolledClipView: method
I'm not sure if my solution is any better than what you have, but thought I'd offer it anyway. I use this with a print view. I'm not using Auto Layout. It only works with bindings – would need adjustment to work with a data source.
You'll see there's an awful hack to make it work: I just add 0.5 to the value I carefully calculate.
This takes the spacing into account but not the headers, which I don't display. If you are displaying the headers you can add that in the -tableView:heightOfRow: method.
In NSTableView subclass or category:
- (void) sizeHeightToFit {
CGFloat height = 0.f;
if ([self.delegate respondsToSelector:#selector(tableView:heightOfRow:)]) {
for (NSInteger i = 0; i < self.numberOfRows; ++i)
height = height +
[self.delegate tableView:self heightOfRow:i] +
self.intercellSpacing.height;
} else {
height = (self.rowHeight + self.intercellSpacing.height) *
self.numberOfRows;
}
NSSize frameSize = self.frame.size;
frameSize.height = height;
[self setFrameSize:frameSize];
}
In table view delegate:
// Invoke bindings to get the cell contents
// FIXME If no bindings, use the datasource
- (NSString *) stringValueForRow:(NSInteger) row column:(NSTableColumn *) column {
NSDictionary *bindingInfo = [column infoForBinding:NSValueBinding];
id object = [bindingInfo objectForKey:NSObservedObjectKey];
NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
id value = [[object valueForKeyPath:keyPath] objectAtIndex:row];
if ([value isKindOfClass:[NSString class]])
return value;
else
return #"";
}
- (CGFloat) tableView:(NSTableView *) tableView heightOfRow:(NSInteger) row {
CGFloat result = tableView.rowHeight;
for (NSTableColumn *column in tableView.tableColumns) {
NSTextFieldCell *dataCell = column.dataCell;
if (![dataCell isKindOfClass:[NSTextFieldCell class]]) continue;
// Borrow the prototype cell, and set its text
[dataCell setObjectValue:[self stringValueForRow:row column:column]];
// Ask it the bounds for a rectangle as wide as the column
NSRect cellBounds = NSZeroRect;
cellBounds.size.width = [column width]; cellBounds.size.height = FLT_MAX;
NSSize cellSize = [dataCell cellSizeForBounds:cellBounds];
// This is a HACK to make this work.
// Otherwise the rows are inexplicably too short.
cellSize.height = cellSize.height + 0.5;
if (cellSize.height > result)
result = cellSize.height;
}
return result;
}
Just get -[NSTableView frame]
NSTableView is embed in NSScrollView, but has the full size.

Resizing UITableView

I'm working on the project. Currently I'm facing a problem, I have 3 UITableView's in one ViewController. It looks something like this:
Now, The first TableView(top one), has just one row, that has a size of 55 pixels and it don;t have to be resizable. Next, second one is the TableView, again with one row, but this row must to resize depends on the content of it. Right now this TableView displaying this row:
This row must to resize depends on how long the text in it. What I did to calculate the size is:
if(tableView == self.usersPostOnTheWallTableView) {
NSDictionary *propertyItems = [self.repliesItems objectAtIndex:indexPath.row];
NSString *text = [self.postItems objectForKey:#"text"];
NSLog(#"%#", text);
CGSize constraintSize = CGSizeMake(280, MAXFLOAT);
CGSize sizeText = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
self.height = MAX(sizeText.height, 44.0f);
self.photosArray = [propertyItems objectForKey:#"attach_photos"];
self.videosArray = [propertyItems objectForKey:#"attach_videos"];
if((self.photosArray.count == 0) && (self.videosArray.count == 0)) {
height += (CELL_CONTENT_MARGIN * 2);
} else if ((self.videosArray.count > 0) && (self.photosArray.count == 0)) {
height += 90 + (CELL_CONTENT_MARGIN * 2);
} else if((self.photosArray.count > 0) && (self.videosArray.count == 0)) {
height += 85 + (CELL_CONTENT_MARGIN * 2);
}
}
Now, if the text is quite small, it fits the row well, and the row is resizes. Such as this:
But, after the text comes bigger, this what happening:
Obviously I don't need the scrolling right there, and I would disable it. But the thing here is that it takes the size of the text, and it resize the label, you can see how many space it make from the top and from the bottom of the label. But it don't put actual text there. Another thing is that it didn't resize the TableView it self, when I'm trying to resize it, I get the overlapping of the bottom table by the middle table.
Can anyone recommend any solution of this, or maybe another way of implementation?
Thanks in advance!
U can do this is use tableviewDelegate method for setting cell height according your text like this for example:
This method will be called for every cell to decide how much cell should have height
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *strDetails = [yourTextArray objectAtIndex:indexPath.row];
//if(indexPath.row == 1)
//{
CGSize stringSize = [strDetails sizeWithFont:[UIFont fontWithName:#"Marker Felt" size:13] constrainedToSize:CGSizeMake(320, 9999) lineBreakMode:UILineBreakModeWordWrap]; // Provide text as well as font name in which your text will be displayed and constrainedToSize which defines width and height it should be maximum
return stringSize.height+65; //Here 65 can be any value depending on Calculation ImageView height + uppermost space + bottom space added
//}
//else
//{
// return 80 // anything u wishes
//}
}

Adjust height of Storyboard-created UITableview to height of its contents

I've dragged a table view into my storyboard scene, but its height is static and won't adjust based on how many cells it contains unless I manually adjust it in storyboard. How can I get this table to adjust its own height based on the total height of its contents?
You're going to have to do some version of this:
Figure out how many cells will be displayed
Figure out the height for each cell
Do the math and set the height of the UITableView
Before you get mad (since I know that's pretty obvious), let me follow up with this:
If you know for sure how many cells will be displayed all the time (and it's a static value), you can just manually adjust the height in Interface Builder.
If you don't know for sure (which I'm guessing is the case, based on your question), you'll have to do #'s 1 thru 3 above at runtime. Then, because UITableView is a subclass of UIView, you can simply adjust its frame property.
For example:
I've got 7 cells.
Cells 1, 3 and 5 (indices 0, 2 and 4) are 20 pixels tall.
Cells 2, 4, 6 and 7 (indices 1, 3, 5 and 6) are 30 pixels tall.
I need a total display height of 180 pixels (you might actually need to play with this because of separators, etc..)
So, I can just call:
CGFloat newHeight = ... // Whatever you calculate
CGRect currentFrame = [[self MyTableView] frame];
[[self MyTableView] setFrame:CGRectMake(
currentFrame.origin.x,
currentFrame.origin.y,
currentFrame.size.width,
newHeight)
];
If you want to get really fancy, you can animate the change by wrapping it in a [UIView animationWithDuration:] block.
Hope this helps!
Update/Edit
Here's a kludgy way to do it... it works, but I'm sure there's a better way to do it. I do not claim this to be the best/fastest/most efficient way. This is more to show a principle. ;-)
- (CGFloat)totalHeightNeededForTableView:(UITableView *)tv {
CGFloat retVal = 0;
int sectionCount = [self numberOfSectionsInTableView:tv];
int rowCount = 0;
NSIndexPath *path = nil;
for (int i = 0; i < sectionCount; i = i + 1) {
rowCount = [self tableView:tv numberOfRowsInSection:i];
for (int j = 0; j < rowCount; j = j + 1) {
path = [NSIndexPath indexPathForRow:j inSection:i];
retVal = retVal + [self tableView:tv heightForRowAtIndexPath:path];
}
}
return retVal;
}

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.