Cell-based NSTableView called function when resizing spot is double clicked? - macos

When you use default cell-based NSTableView and double click resizing spot in column, that column will resize to that width what is max longest text in your content.
What function I must call in view-based NSTableView to get same effect?
Edit: Function what I missed was:
func tableView(tableView: NSTableView, sizeToFitWidthOfColumn column:Int) -> CGFloat
But how can I get max width from my all stringValues?

You can get the maximum width of all rows in a column like this:
var width: CGFloat = 0
for i in 0...(tableView.numberOfRows-1) {
let view = tableView.viewAtColumn(column, row: i, makeIfNecessary: true) as! NSTableCellView
let size = view.textField!.attributedStringValue.size()
width = max(width, size.width)
}
return width+20 //add an optional padding

Related

How to properly create a NSTableCellView with a table that has dynamically created columns

I'm trying to create a desktop application for macOS using Swift 3.
My window has two view-based tables, where one table shows a list of packets with the size of the payload. When a packet is selected, the second table shows the payload bytes. Since the payload size varies the second table has programmatically created columns; the largest packet size selected determine the maximum number of columns.
I have no problems if I use cell-based tables, but I prefer to use view-based for both tables.
The columns are created
for i in numCols ..< maxCols {
let col = NSTableColumn()
col.identifier = "Byte\(i)ColumnID"
col.headerCell.stringValue = "[\(i)]"
tableViewPayload.addTableColumn(col)
}
The problem appears in the viewFor delegate method, where
tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: self) as? NSTableCellView
returns nil
It works if I return a NSTextField directly or create a NSTableCellView with a NSTextField as a subview:
let frameRect = NSRect(x: 0, y: 0, width: (tableColumn?.width)!, height: 20)
let cell = NSTableCellView(frame: frameRect)
cell.identifier = (tableColumn?.identifier)!
let textField = NSTextField(frame: cell.frame)
textField.stringValue = "Test"
cell.addSubview(textField)
return cell
This approach requires more styling of the cell and I should probably subclass NSTableCellView.
My question is why doesn't the make(withIdentifier:owner:) method work with dynamically created columns? Am I missing something when I create the columns?
Is there a way I can create a NSTableCellView and make use of the textField property?
I build entire NSTableViews up programmatically. Assume you are asking about the NSTableView.makeView method. I'm using swift 4.
You know any column you create for the table gets bound to it, and vis versa?
For each column created for the table, I call
column.tableView = tableview
tableview.addTableColumn(column)

In a view-based NSTableView, the NSTextField is clipped by an NSImageView

TL;DR:
In macOS 10.11, a view based NSTableView containing an NSTextField and an NSImageView right under the textfield, with some rows having an image and others not, some texts are clipped after having scrolled the table down then up.
Before scrolling:
After scrolling:
When I say "clipped" it means that the text view is at its minimum height as defined with autolayout, when it should be expanded instead.
Note that this faulty behavior only happens in macOS 10.11 Mavericks. There's no such issues with macOS 10.12 Sierra.
Context
macOS 10.11+, view based NSTableView.
Each row has a textfield, and an image view just below the textfield.
All elements have autolayout constraints set in IB.
Goal
The text view has to adapt its height vertically - the image view should move accordingly, staying glued to the bottom of the textfield.
Of course the row itself also has to adapt its height.
Sometimes there's no image to display, in this case the image view should not be visible.
This is how it's supposed to render when it works properly:
Current implementation
The row is a subclass of NSTableCellView.
In the cell the textfield is set with an attributed string.
In tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat, I make a dummy textView that I use to find the actual textfield's height, and return an amended row height accordingly. I also check if there's an image to display and if it's the case I add the image height to the row height.
let ns = NSTextField(frame: NSRect(x: 0, y: 0, width: defaultWidth, height: 0))
ns.attributedStringValue = // the attributed string
var h = ns.attributedStringValue.boundingRect(with: ns.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading]).height
if post.hasImage {
h += image.height
}
return h + margins
In tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? I prepare the actual cell contents:
getUserAvatar { img in
DispatchQueue.main.async {
cell.iconBackground.layer?.backgroundColor = self.prefs.colors.iconBackground.cgColor
cell.iconBackground.rounded(amount: 6)
cell.iconView.rounded(amount: 6)
cell.iconView.image = img
}
}
cell.usernameLabel.attributedStringValue = xxx
cell.dateLabel.attributedStringValue = xxx
// the textView at the top of the cell
cell.textLabel.attributedStringValue = xxx
// the imageView right under the textView
if post.hasImage {
getImage { img in
DispatchQueue.main.async {
cell.postImage.image = img
}
}
}
Issues
When scrolling the tableView, there's display issues as soon as one or several rows have an image in the image view.
Clipped text
Sometimes the text is clipped, probably because the empty image view is masking the bottom part of the text:
Normal
Clipped
IMPORTANT: resizing the table triggers a redraw and fixes the display issue...
What I've tried
Cell reuse
I thought the main issue was because of cell reuse by the tableView.
So I'm hiding the image field by default in tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?, only unhiding if there's an image to set, and I do it on the main thread:
guard let cell = tableView.make(withIdentifier: "xxx", owner: self) as? PostTableCellView else {
return nil
}
DispatchQueue.main.async {
cell.postImage.isHidden = true
cell.postImage.image = nil
}
// set the text view here, the buttons, labels, etc, then this async part runs:
downloadImage { img in
DispatchQueue.main.async {
cell.postImage.isHidden = false
cell.postImage.image = img
}
}
// more setup
return cell
But the imageView is still blocking the text view in some cases.
And anyway, sometimes the text is clipped at the first display, before any scrolling is done...
CoreAnimation Layer
I thought maybe the cells need to be layer backed so that they're correctly redisplayed, so I've enabled CoreAnimation layer on the scrollView, the tableView, the tableCell, etc. But I hardly see any difference.
A/B test
If I remove the imageView completely and only deal with the textfield everything works ok. Having the imageView is definitely what is causing issues here.
Autolayout
I've tried to handle the display of the row contents without using autolayout but to no avail. I've also tried to set different constraints but clearly I suck at autolayout and didn't manage to find a good alternative to my current constraints.
Alternative: NSAttributedString with image
I've tried to remove the image view and have the image added at the end of the attributed string in the text view instead. But the result is often ugly when it works - and for most times it just doesn't work (for example when the image can't be downloaded in time to be added to the attributed string, leaving the cell without text or image at all and at a wrong height).
Question
What am I doing wrong? How could I fix these issues? My guess is that I should change the autolayout constraints but I don't see a working solution.
Or maybe would you do this entirely differently?
Text field has "Preferred Width" field in IB which allows tune up correlation of intrinsic size with auto-layout calculated size.
Changing this IB property value to "First Runtime Layout Width" or "Explicit" often helps resolving similar issues.
This resolved a lot of my issues in past.

Set NSTextField frame height in tableView viewForTableColumn not working

I have a .xib with 2-3 NSTextFields displayed in it (as shown below). I would like to resize one of these NSTextFields so it best fits the content it is displaying. My problem is that I cannot change the size of the Text Field in the viewForTableColumn at all.
I have successfully set my heightOfRow. That is working nicely.
I have turned off AutoLayout for the field as it wasn't quite working properly for me.
Now in tableView viewForTableColumn I have the following code:
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?
{
let cell = tableView.makeViewWithIdentifier("MessageView", owner: self) as! MessageView!
var newFrame = cell.messageSubject.frame
newFrame.size.height = 200
cell.messageSubject.frame = newFrame
return cell
}
As you can see the frame for the Subject field remains unaltered.
Interestingly the size of the frame is always the default size set in the Interface builder even when AutoLayout is turned on.
My conclusion is that I'm trying to do this in the wrong method.
I have been able to resize the frame in a simple non-table based viewController with no problem.

square table view cell constraint

i have a problem with auto layout. I have UIImageView inside a cell. The image is square and i set the cell height to be 320. but when i use iPad simulator, the cell height isn't change. it look like still use 320 height.
I've try to set Bottom Margin and Top margin to be < or equal to 12 and set the priority to 1000 which is make the image in the correct size but the cell height isn't change.
question :
how i can set the cell height based on the device?
i use Swift.
thanks.
You can set the constant of your constraint to your customized height.
Or use a custom cell height and then override the method func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat to return the customized height based on your device type. And set the top, bottom, leading and trailing values to its superview bounds.
The second solution is as follows: set your constraints to the bounds
Then use a custom height
And finally override the data source method
NBlet iPad = (UIDevice.currentDevice().userInterfaceIdiom == .Pad)

How to let NSTextField grow with the text in auto layout?

Auto layout in Lion should make it fairly simple to let a text field (and hence a label) grow with text it holds.
The text field is set to wrap in Interface Builder.
What is a simple and reliable way to do this?
The method intrinsicContentSize in NSView returns what the view itself thinks of as its intrinsic content size.
NSTextField calculates this without considering the wraps property of its cell, so it will report the dimensions of the text if laid out in on a single line.
Hence, a custom subclass of NSTextField can override this method to return a better value, such as the one provided by the cell's cellSizeForBounds: method:
-(NSSize)intrinsicContentSize
{
if ( ![self.cell wraps] ) {
return [super intrinsicContentSize];
}
NSRect frame = [self frame];
CGFloat width = frame.size.width;
// Make the frame very high, while keeping the width
frame.size.height = CGFLOAT_MAX;
// Calculate new height within the frame
// with practically infinite height.
CGFloat height = [self.cell cellSizeForBounds: frame].height;
return NSMakeSize(width, height);
}
// you need to invalidate the layout on text change, else it wouldn't grow by changing the text
- (void)textDidChange:(NSNotification *)notification
{
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}
Swift 4
Editable Autosizing NSTextField
Based on Peter Lapisu's Objective-C post
Subclass NSTextField, add the code below.
override var intrinsicContentSize: NSSize {
// Guard the cell exists and wraps
guard let cell = self.cell, cell.wraps else {return super.intrinsicContentSize}
// Use intrinsic width to jive with autolayout
let width = super.intrinsicContentSize.width
// Set the frame height to a reasonable number
self.frame.size.height = 750.0
// Calcuate height
let height = cell.cellSize(forBounds: self.frame).height
return NSMakeSize(width, height);
}
override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
super.invalidateIntrinsicContentSize()
}
Setting self.frame.size.height to 'a reasonable number' avoids some bugs when using FLT_MAX, CGFloat.greatestFiniteMagnitude or large numbers. The bugs occur during operation when the user select highlights the text in the field, they can drag scroll up and down off into infinity. Additionally when the user enters text the NSTextField is blanked out until the user ends editing. Finally if the user has selected the NSTextField and then attempts to resize the window, if the value of self.frame.size.height is too large the window will hang.
The accepted answer is based on manipulating intrinsicContentSize but that may not be necessary in all cases. Autolayout will grow and shrink the height of the text field if (a) you give the text field a preferredMaxLayoutWidth and (b) make the field not editable. These steps enable the text field to determine its intrinsic width and calculate the height needed for autolayout. See this answer and this answer for more details.
Even more obscurely, it follows from the dependency on the text field's editable attribute that autolayout will break if you are using bindings on the field and fail to clear the Conditionally Sets Editable option.

Resources