Multiple NSTableCellView in one NSTableviewColumn Cocoa OSX - objective-c

I am trying to create a NSTableview with different cells in one column. I am setting two NSTableCellView's in one column like so:
And setting the data code like so:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
// Get a new ViewCell
// Since this is a single-column table view, this would not be necessary.
// But it's a good practice to do it in order by remember it when a table is multicolumn.
if( [tableColumn.identifier isEqualToString:#"BugColumn"] )
{
NSMutableDictionary *dic =(NSMutableDictionary*)[self.ArraRecentDataLoad objectAtIndex:row];
if([[dic valueForKey:#"type"] isEqualToString:#"image/png"] || [[dic valueForKey:#"type"] isEqualToString:#"image/jpeg"])
{
ImageCell *cellView = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
cellView = [[ImageCell alloc] initWithFrame:NSMakeRect(0, 0, 316, 313)];
// This allows the cell to be reused.
cellView.identifier =tableColumn.identifier;
NSColor *orangeColor = [NSColor colorWithCGColor:[NSColor colorWithRed:240/255.0f green:240/255.0f blue:240/255.0f alpha:1.0f].CGColor];
// Convert to CGColorRef
NSInteger numberOfComponents = [orangeColor numberOfComponents];
CGFloat components[numberOfComponents];
CGColorSpaceRef colorSpace = [[orangeColor colorSpace] CGColorSpace];
[orangeColor getComponents:(CGFloat *)&components];
CGColorRef orangeCGColor = CGColorCreate(colorSpace, components);
[cellView.bgCellview setWantsLayer:YES];
cellView.bgCellview.layer.masksToBounds= YES;
cellView.bgCellview.layer.borderWidth=2;
// Set border
cellView.bgCellview.layer.borderColor = orangeCGColor;
// Clean up
CGColorRelease(orangeCGColor);
return cellView;
}
else
{
HomeCell *cellView = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
NSColor *orangeColor = [NSColor colorWithCGColor:[NSColor colorWithRed:240/255.0f green:240/255.0f blue:240/255.0f alpha:1.0f].CGColor];
// Convert to CGColorRef
NSInteger numberOfComponents = [orangeColor numberOfComponents];
CGFloat components[numberOfComponents];
CGColorSpaceRef colorSpace = [[orangeColor colorSpace] CGColorSpace];
[orangeColor getComponents:(CGFloat *)&components];
CGColorRef orangeCGColor = CGColorCreate(colorSpace, components);
[cellView.bgview setWantsLayer:YES];
cellView.bgview.layer.masksToBounds= YES;
cellView.bgview.layer.borderWidth=2;
// Set border
cellView.bgview.layer.borderColor = orangeCGColor;
// Clean up
CGColorRelease(orangeCGColor);
return cellView;
}
}
return nil;
}
But in my xib if I drag a second cell (imageCell) then my app crashes with unrecognized select. If I do it like the above screenshot, then I am not able to load a second cell and there is white space in NSTableview.
I do lots of RND but I am not able to find a proper way to use different cells in a single column and load as per conditional data.
Note:
If I remove alloc cell in first condition:
cellView = [[ImageCell alloc] initWithFrame:NSMakeRect(0, 0, 316, 313)];
cellView.identifier =tableColumn.identifier;
Then that shows the first row's pre-define labels instead of second ImageCell predefine like iOS.

The two cell views need to have different identifiers. You will have to manually assign at least one of them. You can't let Xcode automatically assign both. (I'm not even sure if Xcode will let you configure things that way.)
Then, you need to use the proper identifier when you call [tableView makeViewWithIdentifier:... owner:self]. At most one of those calls can use the table column's identifier. The other must use the manually-assigned one.
If you put both cell views in the NIB (with separate identifiers) and fetch them using [tableView makeViewWithIdentifier:... owner:self], then you don't need to alloc/init one programmatically (and you shouldn't). If you prefer to alloc/init one programmatically, you must assign a different identifier for it. Again, you can't have two separate cell view types with the same identifier.
Update: Let me try again in simpler form. Do this:
In the NIB, select the Image Cell view. On the Identity inspector, set the identifier to "ImageCell".
In your code, for the image branch, do ImageCell *cellView = [tableView makeViewWithIdentifier:#"ImageCell" owner:self]; instead of using the table column identifier.
Remove the following two statements which allocate a new instance of the ImageCell class and sets its identifier property.
So, the code should look like this:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
// Get a new ViewCell
// Since this is a single-column table view, this would not be necessary.
// But it's a good practice to do it in order by remember it when a table is multicolumn.
if( [tableColumn.identifier isEqualToString:#"BugColumn"] )
{
NSMutableDictionary *dic =(NSMutableDictionary*)[self.ArraRecentDataLoad objectAtIndex:row];
if([[dic valueForKey:#"type"] isEqualToString:#"image/png"] || [[dic valueForKey:#"type"] isEqualToString:#"image/jpeg"])
{
ImageCell *cellView = [tableView makeViewWithIdentifier:#"ImageCell" owner:self];
NSColor *orangeColor = [NSColor colorWithCGColor:[NSColor colorWithRed:240/255.0f green:240/255.0f blue:240/255.0f alpha:1.0f].CGColor];
// ...

See first of all make it view base table view from nib. Then in viewfortablecolumn put conditions like of image, then check first of view(image cell or home cell make it simple NSView) available for image type then use it or alloc using makeviewusingidentifire or what ever you use and return that view. On second condition you have do same for that.
For every time call this method it will check first which type of view (image cell or home cell in this particular) it wants then check if it was available in memory then use it if not then alloc and use it.
Let me know if any problem on this.
Sry for bad English

Related

How do I change the color or a NSTableCellView for each cell of an NSOutlineView

I've looked at a number of questions but can't find a good solution for a View-Based NSOutlineView
Coloring NSTableView Text per row
Change color of NSTableViewCell
Custom background colors for NSTableCellView
I'm trying to set each row to whatever color I want. I've read somewhere that I need to subclass NSTableRowView which I've now done.
According to the AppleDocs, I see the following methods:
– drawBackgroundInRect:
– drawDraggingDestinationFeedbackInRect:
– drawSelectionInRect:
– drawSeparatorInRect:
How would I go about setting the background color for the individual rows? Am I going the wrong route above?
Edit: below (also edited title)
Since i'm using an NSOutlineView and not a NSTableView, when i change the background color of the cells the image looks like the following. The disclosure arrows to the left is not colored. Is there any way to change the color of the whole row for the NSOutlineView?
You could subclass NSTableViewCell, and add a method to it which sets its color.
NSTableViewCell is already a subclass of NSView, so in your subclass, you would add the following method:
- (void)setBackgroundColor {
self.layer.backgroundColor = CGColorCreateGenericRGB(0, 0, 0, 1.0f); // or whatever color
}
Or something like that. You'll probably want to have the color be a param to the method. Then, in your table view delegate, you can set the color depending on the row index passed to the delegate method. For example:
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [table dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"Cell"];
if (indexPath.row % 2) {
[cell setBackgroundColor:[UIColor redColor]]; // or something like that
}
}
Came up with a solution. Implemented the following.
-(void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row {
[self updateRowViewBackColorforItem:[outlineView itemAtRow:row]];
}
-(void)updateRowViewBackColorforStep:(myCustomItem *)customItem {
static NSColor *color1;
static NSColor *color2;
static NSColor *color3;
if (color1 == nil) {
sharedcolorHeader = [NSColor colorWithCalibratedRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:1.0];
}
if (color2 == nil) {
sharedcolorChildren = [NSColor colorWithCalibratedRed:(x/255.0f) green:(y/255.0f) blue:(z/255.0f) alpha:1.0];
}
if (color3 == nil) {
normalColor = [NSColor colorWithCalibratedRed:(255/255.0f) green:(255/255.0f) blue:(255/255.0f) alpha:1.0];
}
NSInteger row = [stepOutlineView rowForItem:step];
if (row < 0) return;
NSTableRowView *view = [myOutlineView rowViewAtRow:row makeIfNecessary:NO];
if ([customItem type] == 1) {
[view setBackgroundColor:sharedcolorHeader];
} else if([customItem type] == 2) {
[view setBackgroundColor:sharedcolorChildren];
} else {
[view setBackgroundColor:normalColor];
}
}
This is really something that should rely on properties or ivars in your data model.
If you use view based outline views, you can simply have custom views for your row views and or cell views.
Have the custom views draw whatever you want based in the data in your represented object.

reloading a uitableview starts at the 2nd row (or not at the first row)

Uitableview problem again. Whenever I reload my data, the tableview would reload but it seems like the data will not start at the first row. Please refer to the image below.
I tried the following after the reload of data but still no success:
[self.tableView reloadData];
// this or the other one ... [self.tableView setContentOffset:CGPointZero animated:NO];
[self.tableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO];
This is how I positioned the uitableview on viewdidload
self.tableView.frame = CGRectMake(450, 20, self.tableView.frame.size.width, self.tableView.frame.size.height - 20);
EDIT 1:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"SearchResultCell";
User *user = [users objectAtIndex:indexPath.row];
UserTableCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[UserTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.name.text = [NSString stringWithFormat:#"%# %#", user.firstName, user.lastName];
return cell;
}
Based on your comments, your table is being resized incorrectly when you are rotating the device to landscape mode. In your storyboard, you need to set the autoresizing on the table view in order for it to properly resize the way that you want.
Here is a good starting point for something like this:
Notice just above the Autosizing label, it shows a bunch of red bars (called springs and struts). I have them all turned on, which will maintain the distance from the edge of the superview and allow the object to change its' size in order to keep those distances the same. This should be close to what you want, and if not then you can play around with the different combinations until you get what you want.
You turn each part of it on and off by clicking on the red line.
For more details, take a look at this question: Autoresizing masks programmatically vs Interface Builder / xib / nib

SetNeedsDisplay in table view

My problem is that not all cell are correct initialized with the correct UIView as an rendered image.( see code below) On a iphone 4 it always the same cel on a iphone with retina display is also an other cell. In this set-up setNeedsDisplay won't function.
If i use the same structure in an IBoutlet it's working.
!
I need to use in some cells the image-files .png and at some other cells the defined drawing method, which uses the drawrect or better i should use the setNeedsDisplay method.....
What is going wrong!!!
my code
tableview... cell
static NSString *CellIdentifier = #"TypeCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
CellTypes *cellType = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = cellType.title;
cell.detailTextLabel.text = cellType.detail;
if ( [cellType.type rangeOfString:#"TL"].location != NSNotFound) {
cell.imageView.image = [UIImage imageNamed:[cellType.type stringByAppendingString:#"Thumb"]];
cell.indentationWidth = 10;
}
else {
static CGFloat scale = 0.0; // old API , screen values
UIScreen *screen = [UIScreen mainScreen];
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 4.0) {
scale = [screen scale];
}
if (scale>0.0) {
UIGraphicsBeginImageContextWithOptions(cell.imageView.bounds.size, NO, scale);
}
else {
UIGraphicsBeginImageContext(cell.imageView.bounds.size);
}
CGRect imageRect = CGRectMake(0, 0, 84, 84 ); // cell.imageView.bounds;
FittingImageView *fittingImage = [[FittingImageView alloc] initWithFrame:imageRect];
fittingImage.thumb = YES;
fittingImage.title = offSetType.title;
[fittingImage drawRect:imageRect]; // this works but skips one imageRect
// [fittingImage setNeedsDisplay]; //this won't work.
[cell.layer renderInContext:UIGraphicsGetCurrentContext()];
cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
return cell;
You can never call drawRect: yourself. It is called by Cocoa, and only Cocoa can setup the context. It's unclear what you're trying to do here. I don't know what a FittingImageView is. You're creating it and then throwing it away in any case.
You then try to render the cell itself into an image, and the put that image into the cell's imageview. That doesn't make sense.
You may misunderstand how tableview cells are created, reused and drawn. You should re-read "Creating and Configuring a Table View."
The primary things to remember:
In this routine, your job is to either create a new cell, or reconfigure a reusable cell (if dequeueReusableCellWithIdentifier: returns you something).
This routine is not for drawing a cell. That will happen much later, and it is the cell's job to draw itself. You're just configuring the cell. Putting everything in it that it needs to draw later.
You typically do not want to compute expensive things in this routine. Compute those elsewhere and cache them. This routine should configure a cell, slap data into it, and return. If you try to create new images in this routine, your table view is going to stutter.
You shouldn't mess with the cell's layer in the middle of this routine. If you need something that complex, you should be creating a custom cell with its own drawRect:.

Add a second subtitle row to UITableViewCellStyleSubtitle

I have already made a custom class with the init:
if(self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]) {
}
As I wanted to add a blue texted label to the right hand side of the cell and have it go right when selected otherwise it looked odd when highlighted:
self.sizeTextLabel = [[UILabel alloc] initWithFrame:CGRectMake(self.contentView.frame.size.width-110, 0.0, 100.0, 44.0)];
self.sizeTextLabel.textAlignment = NSTextAlignmentRight;
self.sizeTextLabel.backgroundColor = [UIColor whiteColor];
self.sizeTextLabel.textColor = [UIColor colorWithRed:81/255.0 green:102/255.0 blue:145/255.0 alpha:1.0];
self.sizeTextLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleHeight;
[self.contentView addSubview:self.sizeTextLabel];
There is probably a better way of doing that ^
What I would like to do is add another label that is effectively another subtitle row, I then increase the height of the row when the cell is created with the heightForRowAtIndexpath method.
Problem: When I add a new label 'row' to the content view, it does not get higher (the default views shift to the middle of the view). How do I create and position it correctly below the subtitle view? If I were to change the first subtitle to multiline it would be nice if the second label knew what to do.
I wish cocoa had relative positioning. Or I haven't found it yet!
You can make the detail text label multi line and add your two strings to the one label. In this example, my data has three keys in its dictionaries, "main" for the main label text, and "detail1 and "detail2" for the two subtitle strings.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
NSDictionary *object = _objects[indexPath.row];
cell.textLabel.text = object[#"main"];
NSString *concat = [NSString stringWithFormat:#"%#\n%#",object[#"detail1"],object[#"detail2"]];
cell.detailTextLabel.numberOfLines = 2;
cell.detailTextLabel.text = concat;
return cell;
}
You can make a custom cell with interface builder and use it instead of UITableViewCell.
Check this: http://www.backslashtraining.com/blog/2012/3/10/ios-5-tip-of-the-week-custom-table-view-cells.html

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.