for clarification I'll just add 2 overlaid Screenshots, one in Interface Builder, the other on the device.
The lower UISegmentedControl is fresh out of the library with no properties edited, still it looks different on the Device (in this case a non-Retina iPad, though the problem is the same for Retina-iPhone) (Sorry for the quick and dirty photoshopping)
Any ideas?
EDIT: I obviously tried the "alignment" under "Control" in the Utilities-Tab in Interface Builder. Unfortunately none of the settings changed anything for the titles in the UISegment. I don't think they should as they are not changing titles in Interface Builder either.
EDIT2: Programmatically setting:
eyeSeg.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
doesn't make a difference either.
Found the Problem "UISegmentedControlStyleBezeled is deprecated. Please use a different style."
See also what-should-i-use-instead-of-the-deprecated-uisegmentedcontrolstylebezeled-in-io
Hmm...have you checked the alignment? Maybe that's the case.
You can recursively search the subviews of the UISegmentedControl view for each of the UILabels in the segmented control and then change the properties of each UILabel including the textAlignment property as I've shown in a sample of my code. Credit to Primc's post in response to Change font size of UISegmented Control for suggesting this general approach to customizing the UILabels of a UISegmentedControl. I had been using this code with the UISegmentedControlStyleBezeled style by the way even after it was deprecated although I have recently switched to UISegmentedControlStyleBar with an adjusted frame height.
- (void)viewDidLoad {
[super viewDidLoad];
// Adjust the segment widths to fit the text. (Will need to calculate widths if localized text is ever used.)
[aspirationControl setWidth:66 forSegmentAtIndex:0]; // Navel Lint Collector
[aspirationControl setWidth:48 forSegmentAtIndex:1]; // Deep Thinker
[aspirationControl setWidth:49 forSegmentAtIndex:2]; // Mental Wizard
[aspirationControl setWidth:64 forSegmentAtIndex:3]; // Brilliant Professor
[aspirationControl setWidth:58 forSegmentAtIndex:4]; // Nobel Laureate
// Reduce the font size of the segmented aspiration control
[self adjustSegmentText:aspirationControl];
}
- (void)adjustSegmentText:(UIView*)view {
// A recursively called method for finding the subviews containing the segment text and adjusting frame size, text justification, word wrap and font size
NSArray *views = [view subviews];
int numSubviews = views.count;
for (int i=0; i<numSubviews; i++) {
UIView *thisView = [views objectAtIndex:i];
// Typecast thisView to see if it is a UILabel from one of the segment controls
UILabel *tmpLabel = (UILabel *) thisView;
if ([tmpLabel respondsToSelector:#selector(text)]) {
// Enlarge frame. Segments are set wider and narrower to accomodate the text.
CGRect segmentFrame = [tmpLabel frame];
// The following origin values were necessary to avoid text movement upon making an initial selection but became unnecessary after switching to a bar style segmented control
// segmentFrame.origin.x = 1;
// segmentFrame.origin.y = -1;
segmentFrame.size.height = 40;
// Frame widths are set equal to 2 points less than segment widths set in viewDidLoad
if ([[tmpLabel text] isEqualToString:#"Navel Lint Collector"]) {
segmentFrame.size.width = 64;
}
else if([[tmpLabel text] isEqualToString:#"Deep Thinker"]) {
segmentFrame.size.width = 46;
}
else if([[tmpLabel text] isEqualToString:#"Mental Wizard"]) {
segmentFrame.size.width = 47;
}
else if([[tmpLabel text] isEqualToString:#"Brilliant Professor"]) {
segmentFrame.size.width = 62;
}
else {
// #"Nobel Laureate"
segmentFrame.size.width = 56;
}
[tmpLabel setFrame:segmentFrame];
[tmpLabel setNumberOfLines:0]; // Change from the default of 1 line to 0 meaning use as many lines as needed
[tmpLabel setTextAlignment:UITextAlignmentCenter];
[tmpLabel setFont:[UIFont boldSystemFontOfSize:12]];
[tmpLabel setLineBreakMode:UILineBreakModeWordWrap];
}
if (thisView.subviews.count) {
[self adjustSegmentText:thisView];
}
}
}
The segmented control label text has an ugly appearance in IB but comes out perfectly centered and wrapped across 2 lines on the device and in the simulator using the above code.
Related
I'm trying to show some extra symbols next to lines in NSTextView, based on text attributes.
I have successfully subclassed NSLayoutManager, but it seems that layout manager can't draw outside the area set by textContainerInset.
Because my text view can potentially have a very long strings, I'm hoping to keep the drawing connected to displaying glyphs. Is there a way to trick the layout manager to be able to draw inside the content insets — or is there another method I use instead of drawGlyphsForGlyphRange?
I have tried calling super before and after drawing, as well as storing and not storing graphics state. I also attempted setDrawsOutsideLineFragment:YES for the glyphs, but with no luck.
Things like Xcode editor itself uses change markers, so I know that this is somehow doable, but it's very possible I'm looking from the wrong place.
My drawing method, simplified:
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
NSTextStorage *textStorage = self.textStorage;
NSTextContainer *textContainer = self.textContainers[0];
NSRange glyphRange = glyphsToShow;
NSSize offset = self.textContainers.firstObject.textView.textContainerInset;
while (glyphRange.length > 0) {
NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL], attributeCharRange, attributeGlyphRange;
id attribute = [textStorage attribute:#"Revision" atIndex:charRange.location longestEffectiveRange:&attributeCharRange inRange:charRange];
attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);
if (attribute != nil) {
[NSGraphicsContext saveGraphicsState];
NSRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange
inTextContainer:textContainer];
// Find the top of the revision
NSPoint point = NSMakePoint(offset.width - 20, offset.height + boundingRect.origin.y + 1.0);
NSString *marker = #"*";
[marker drawAtPoint:point withAttributes:#{
NSForegroundColorAttributeName: NSColor.blackColor;
}];
[NSGraphicsContext restoreGraphicsState];
}
glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
glyphRange.location = NSMaxRange(attributeGlyphRange);
}
}
The answer was much more simple than I anticipated.
You can set lineFragmentPadding for the associated NSTextContainer to make more room for drawing in the margins. This has to be taken into account when setting insets for the text container.
How can I add padding to a NSTableCellView?
I want the contents to have a padding of 10px, similar to how you would do it in plain HTML. (All sides). The text should be vertically centered in the middle.
At the moment I'm doing this in a subclass of NSTextFieldCell. But this doesn't seem to work correctly.
When I edit the text, the edit text field does not use the padding from the textfieldcell.
Image 2:
Here is the code I currently have, (subclass of NSTextFieldCell)
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSRect titleRect = [self titleRectForBounds:cellFrame];
NSAttributedString *aTitle = [self attributedStringValue];
if ([aTitle length] > 0) {
[aTitle drawInRect:titleRect];
}
}
- (NSRect)titleRectForBounds:(NSRect)bounds
{
NSRect titleRect = bounds;
titleRect.origin.x += 10;
titleRect.origin.y = 2;
NSAttributedString *title = [self attributedStringValue];
if (title) {
titleRect.size = [title size];
} else {
titleRect.size = NSZeroSize;
}
// We don't want the width of the string going outside the cell's bounds
CGFloat maxX = NSMaxX(bounds);
CGFloat maxWidth = maxX - NSMinX(titleRect);
if (maxWidth < 0) {
maxWidth = 0;
}
titleRect.size.width = MIN(NSWidth(titleRect), maxWidth);
return titleRect;
}
Below are some options for getting the padding.
Earlier I had some problems getting the correct bounding box height for NSAttributedString. I don't remember how I solved them but there are some discussions on the matter.
Idea #1:
Use NSTableView's intercell spacing. It's also available in the Interface Builder from the table view's size tab. Look for cell spacing.
Idea #2:
When editing the interface:
Unless you need something else, use the text or image and text table cell view provided by Apple.
Change the height of the table cell view inside the table view using the size tab.
Reposition the text field inside the table cell view.
Idea #3:
Use a custom cell.
You can change the field editor position by overriding -[NSTextFieldCell editWithFrame:inView:editor:delegate:event:]. There's also -[NSTextFieldCell setUpFieldEditorAttributes]. I found this sample code useful.
If you increase the height of the cell, there are a couple of ways to make NSTextFieldCell draw the text vertically centered.
use the setContentInset method.it sets the distance of the inset between the content view and the enclosing table view.
for example
self.tableView.contentInset = UIEdgeInsetsMake(-35, 0, -20, 0);
I've searched around on how to perform this but I can't find any answer.
I'd like to know if my NSTextfield is already truncating the text (... at the end) without having to check the length of its stringValue. I need to do this to know if I should set a tooltip on my NSTextfield (acting like a simple label).
The reason I don't want to check the length of my textfield's stringValue it's because there are some characters that occupy more space than others, so that's not very accurate
Thanks!
For future visitors, you can use the -[NSCell expansionFrameWithFrame:inView:] method to determine if truncation is taking place. From the header:
Allows the cell to return an expansion cell frame if cellFrame is
too small for the entire contents in the view. ...<snip>... If the frame is not too
small, return an empty rect, and no expansion tool tip view will be
shown. By default, NSCell returns NSZeroRect, while some subclasses
(such as NSTextFieldCell) will return the proper frame when required.
In short, you can tell if an NSTextField is truncating like this:
NSRect expansionRect = [[self cell] expansionFrameWithFrame: self.frame inView: self];
BOOL truncating = !NSEqualRects(NSZeroRect, expansionRect);
Your best bet might be to use an NSTextView instead of a NSTextField. If you use an NSTextView you can get the NSTextContainer of the NSTextView using the textContainer property. The container can tell you the containerSize (the space the text is drawn in).
NSString objects respond to the method sizeWithAttributes:. You can use the resulting NSSize struct to grab the width of the text if drawn with the given attributes. See the "Constants" section of the NSAttributedString Application Kit Additions Reference to figure out what attributes are relevant.
If the containerSize width is less than the sizeWithAttributes: width then the text will be truncated.
EDIT: Apologies, this is only true with no lineFragmentPadding, but the default lineFragmentPadding is non-zero. Either subtract the textContainer.lineFragmentPadding from containerSize.width or use the NSTextContainer setLineFragmentPadding: method to set it to 0.
I suppose you could also make some assumptions about the text area relative to the size of the NSTextField and use the sizeWithAttributes: NSString method in conjunction with that, but that is not as clean.
EDIT 2: I realized I did not address the OP's interest in truncating the text using ellipses. The example code below uses truncation in the NSTextView. I also thought I might as well throw in some code that makes the NSTextView a little more similar in appearance to the NSTextField by putting it inside of a NSBox. Adding a check for size to determine if a tooltip should be displayed would be a simple addition to the code below using the information already mentioned above.
NSString* string = #"Hello World.";
// get the size the text will take up using the default font
NSSize textBounds = [string sizeWithAttributes:#{}];
// Create a border view that looks more or less like the border used around
// text fields
NSBox* borderView = [[NSBox alloc] initWithFrame:NSMakeRect(10, 10, 60, textBounds.height+4)];
[borderView setBoxType:NSBoxCustom];
[borderView setBorderType:NSBezelBorder];
[borderView setContentViewMargins:NSMakeSize(0, 0)];
[borderView setFillColor:[NSColor whiteColor]];
// Create the text view
NSTextView* textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 60, textBounds.height)];
[textView setTextContainerInset:NSMakeSize(2, 0)];
[textView.textContainer setLineFragmentPadding:0];
[textView setEditable:YES];
// Set the default paragraph style so the text is truncated rather than
// wrapped
NSMutableParagraphStyle* parStyle = [[NSMutableParagraphStyle alloc] init];
[parStyle setLineBreakMode:NSLineBreakByTruncatingTail];
// Do not let text get squashed to fit
[parStyle setTighteningFactorForTruncation:0.0];
[textView setDefaultParagraphStyle:parStyle];
[parStyle release];
// Set text
[textView setString:string];
// add NSTextView to border view
[borderView addSubview:textView];
[textView release];
// add NSBox to view you want text view displayed in
[self addSubview:borderView];
[borderView release];
I think you can do it by looking at the frame of the text field's field editor. You check its width in controlTextDidBeginEditing, and then again in controlTextDidEndEditing. If the latter value is larger, then the text has been truncated. The following is implemented in the text field's delegate (I created an ivar for initialWidth):
- (void)controlTextDidBeginEditing:(NSNotification *)aNotification {
if (! initialWidth)
initialWidth = ((NSTextView *)aNotification.userInfo[#"NSFieldEditor"]).frame.size.width;
}
- (void)controlTextDidEndEditing:(NSNotification *)aNotification {
if (initialWidth) { //Make sure that beginEditing is called first
NSInteger finalWidth = ((NSTextView *)aNotification.userInfo[#"NSFieldEditor"]).frame.size.width;
if (finalWidth - initialWidth > 1) NSLog(#"We have truncation");
NSLog(#"Final: %ld Initial: %ld", finalWidth,initialWidth);
}
}
This seems to work most of the time, including if you type in a long string, then delete until it fits again. In a few cases, it gave me the log message when I was one character past the end of the text field, but I did not get the ellipsis when editing ended.
Swift 4 solution using ipmcc as base
It will resize one by one until it fit or fontsize = 3
var size_not_ok = true
var conter = 0
let mininum_font_size = 3 // will resize ultil 3
while size_not_ok || conter < 15 { // will try 15 times maximun
let expansionRect = le_nstextfield.expansionFrame(withFrame: le_nstextfield.frame)
let truncated = !NSEqualRects(NSRect.zero, expansionRect)
if truncated {
if let actual_font_size : CGFloat = le_nstextfield.font?.fontDescriptor.object(forKey: NSFontDescriptor.AttributeName.size) as? CGFloat {
le_nstextfield.font = NSFont.systemFont(ofSize: actual_font_size - 1)
if actual_font_size < mininum_font_size {
break
}
}
} else {
size_not_ok = false
}
conter = conter + 1
}
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.
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.