NSTextView in NSOutlineView with IntrinsicContentSize setting wrong height - swift

I have an outlineView in which I am putting NSTextViews that resize when edited (think outliner app). I have most of this working, but some behaviour is inconsistent.
On my NSOutlineView I set:
outlineView?.usesAutomaticRowHeights = true
For my cell-views I subclass NSTextView. I set the following auto layout bits:
self.translatesAutoresizingMaskIntoConstraints = false
setContentHuggingPriority(NSLayoutConstraint.Priority.defaultHigh, for: NSLayoutConstraint.Orientation.vertical)
And I override the intrinsic content size calculation on the NSTextView:
override var intrinsicContentSize: NSSize {
guard let manager = textContainer?.layoutManager else {
return .zero
}
print("\(manager.usedRect(for: textContainer!).size) \(string)")
return manager.usedRect(for: textContainer!).size
}
(I was calling ensureLayout on the layoutManager in the code above but it adds nothing)
intrinsicContentSize is called twice per text view when they are added to the outliner. The first time the size returned is correct, but on the second call some of the text wraps unnecessarily. A printout of the two passes on intrinsicContentSize for 4 text views are shown below. The column width is 281, so none of these strings should wrap. The first pass they all fit to one line (14 high), on the second pass, the last two strings wrap, which is strange because they are not the longest strings:
(178.744140625, 14.0) New pointwddwek kelekwelek...
(100.720703125, 14.0) Related Subjects
(119.400390625, 14.0) Related Publications
(87.150390625, 14.0) Related Terms
(178.744140625, 14.0) New pointwddwek kelekwelek...
(100.720703125, 14.0) Related Subjects
(74.705078125, 28.0) Related Publications
(54.484375, 28.0) Related Terms
It is consistently the same strings that result in the same behaviour. E.g. the string "Related Subjects" never wraps, the string "Related Terms" always wraps.
When the views are presented, the text is NOT actually wrapped, even thought the usedRect value implies that it would be. The text is shown correctly, but the row view in the outliner is too high because it thinks it has two lines of text.
Any pointers where I might be missing something? Does 'ensureLayout' somehow refer to it's previous calculation and then have rounding issues when fitting the same string into its last-calculated width?

OK, the key here was that the text was presented correctly but the size of the view was wrong.
I created a delagate for the NSTextView's layoutManager. The text was being set out 3 times after the NSTextView was added to the NSOutlineView (which seems to be terribly inefficient!). intrinsicContentSize was only being called on the NSTextView after the first two text layouts.
Calling invalidateIntrinsicContentSize from within layoutManager: didCompleteLayoutFor... fixed everything up.
I still don't understand what is happening here though, and why all these methods are being called so many times when stuff is presented. I guess it is to do with the complexity of auto layout and things pushing against each other.
I also don't understand why only some of the calculations were incorrect during the process.
Please comment here if anyone can shine some light on this!

Related

How to add space between rows in NSTableView

I'm trying to add spaces in between rows in an NSTableView, like how it looks here.
Currently, however, my rows look like this, with 0 spacing between them.
Is it possible to add these spaces? I found this post on how to do it, but that's for UITableView, and I don't think you can add sections with NSTableView. Another thing I tried was using intercellSpacing on the table view, like so:
tableView.intercellSpacing = NSSize(width: 0, height: 80)
However, that just increases the height of each row rather than increase the space between them.
Lastly, I looked into drawSeparator, which seems promising but has limited documentation. Would extending NSTableRowView and overriding the drawSeparator method work, basically by drawing in a blank space as the separator? If so, how would I go about making my table view use my custom row view class?
If none of these options work, I'd also be open to faking the effect, maybe by having the actual content of a row be smaller than the row itself and using the remaining space as the padding between rows. However, I'm not sure if this would work, given that right now I'm using NSShadow, which highlights the boundary of each row.
Found a way to work around this issue. Before, each row consisted of two columns, one for the text fields and one for the buttons. However, I've changed it by putting all the text fields and buttons into a single column, that way there's only one cell per row. I then can apply the NSShadow and other styles to the NSTableCellView rather than the NSTableRowView. This means that I can now use intercellSpacing to create vertical spacing between cells:
tableView.intercellSpacing = NSSize(width: 0, height: 80)
The rows are still touching, but I've disabled the borders/highlighting on them so you can't actually see them. The cells, on the other hand, are visible, and you can adjust the spacing/styles on them as necessary.

How do I fit a paragraph in a scroll view controller programmatically?

I'm building an app and it has a description of everyone on the roster section. The text seems to overrun the page and I do not know the code or way to make it fill programmatically.
I have tried changing the values on the line of code the UITextField is in.
let descriptionM = UITextField(frame: CGRect(x: xPosition+15, y:
500, width: 300, height: 50))
descriptionM.text = desc[i]
descriptionM.textColor = UIColor.black
descriptionM.backgroundColor = UIColor.clear
descriptionM.contentMode = .scaleAspectFill
I wanted the text which is an array above, to fill the certain page on the scroll view instead it runs over the right side of the view with only the first line readable.
I have tried different values but non have worked.
I have tried changing the values on the line of code the UITextField is in.
You're using the wrong tool for the job. Text fields only ever handle one line, which is why there are no properties that let you set the number of lines.
I wanted the text which is an array above, to fill the certain page on the scroll view instead it runs over the right side of the view with only the first line readable.
Views that display text on multiple lines include UILabel and UITextView. UILabel is really meant for just a few lines or less, and it sounds like you may need many lines, so you should switch to UITextView. Or, if you're displaying a number of distinct items rather than paragraphs of text, you could use a UITableView where each cell displays a single item using a UILabel, UITextField, or UITextView. Table views are great for presenting arrays of data.

Number of characters that fit in NSTextView

Note: This question is for a MacOS app and not for iOS
I have a business requirement to convert this amazing iOS custom text view(ReadMoreTextView) to macOS(AppKit). However I am finding it difficult to convert it. I was hoping it would be easy since both UITextView and NSTextView use NSTextContainer, NSLayoutManager and NSTextStorage. But it seems like they behave differently on both platforms.
I need to calculate the number of characters or character range that is visible on the NSTextView. I am trying to use the following method but it is always returning the complete character range instead of visible character range(actual text length instead of visible text length). I couldn't find any other method in layout manager which can help on this. Please let me know if you have any pointers on this.
Here is the code:
extension NSLayoutManager {
func characterRangeThatFits(textContainer container: NSTextContainer) -> NSRange {
//this is the current maximum number of lines
//container.maximumNumberOfLines = 3
var rangeThatFits = self.glyphRange(for: container)
rangeThatFits = self.characterRange(forGlyphRange: rangeThatFits, actualGlyphRange: nil)
return rangeThatFits
}
}
Answers in Swift and Objective C are welcome.
Update:
Please ignore any scrolling on the textview since I am disabling all types of scrolling. My intention is to have a view with "... More" button at the end of it then on tap of it the view gets expanded. For this reason NSTextField based solution is also welcome if we can find visible characters in that.
You can get the visible portion of the NSTextView using its visibleRect method. From there, you just need to get the text range that falls within that rect. So, if your view supports vertical scrolling only, something like this should work:
let visRect = textView.visibleRect
let layoutManager = textView.layoutManager!
let container = textView.textContainer!
let glyphRange = layoutManager.glyphRange(forBoundingRect: visRect, in: container)
let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
Be aware that if your NSTextView supports horizontal scrolling rather than just vertical, you'll have to do a little more work than this, since you'll possibly be looking at a non-contiguous block of text. In that case, you'll have to get a bunch of smaller ranges for each visible line fragment instead of just grabbing the whole range like this.

NSTextField label: dynamically change content and grow width to fit

If I drag a Label (of class NSTextField) into a view in Interface Builder, and then attempt to set its stringValue dynamically like so:
self.testingLabel.stringValue = "Labels are really really really long"
Then most of the label will be cut off.
How can I make the label grow to fit its content without wrapping/scrolling?
I have tried using this custom class but it doesn't work.
class AutogrowTextField: NSTextField {
override var intrinsicContentSize: NSSize {
get {
var frame = self.frame;
frame.size.width = CGFloat.max;
// Calculate new height within the frame
// with practically infinite height.
let width = self.cell!.cellSizeForBounds(frame).width
Swift.print(width)
NSBeep()
Swift.print(self.frame.width)
return NSMakeSize(width, frame.size.height)
}
}
// you need to invalidate the layout on text change, else it wouldn't grow by changing the text
override func textDidChange(notification: NSNotification) {
super.textDidChange(notification)
self.invalidateIntrinsicContentSize()
}
}
Essentially I just want the default behaviour of an HTML document like this:
<p id="dynamic">Hi</p>
<script>
document.getElementByID("dynamic").innerHtml = "Something really really really long"
</script>
This is extremely painful for me as while I am new to Mac OS X and Cocoa, I am not a 'beginner' programmer.
In case it helps here is the contents of the attributes inspector. (it should just be all default values)
Yes, auto layout is enabled
Are you sure your NIB or storyboard is configured to use auto layout? That would be a checkbox on the File inspector.
The reason I ask is that the Attributes inspector would have a "Preferred Width _ First Runtime Layout Width" checkbox, immediately below the "Uses Single Line Mode" checkbox. It doesn't, which makes me think that auto layout is not enabled.
If you're not using auto layout, then you are responsible for either setting the label's frame size or, more simply, telling it to size itself to fit its content by calling sizeToFit().
If you're using auto layout, then the standard NSTextField already computes its intrinsicContentSize based on its string content. The problem is almost certainly with whatever other constraints you may have set on the view. You need to make sure that the auto layout system is free to adjust the label's width. So, you must not set an explicit width constraint. Or, if you have, say, both a leading and trailing spacing constraint, you need to make sure the label's compression-resistance priority is high enough to push the other related views out or compress them. Etc.

Hide a UITableView by setting row height to 0?

I just inherited code which hides/shows rows a UITableView by using the delegate heightForRowAtIndexPath method and returning height 0 for "hidden rows".
The code works, but it has me concerned there might be fraught with unforeseen complications. Can someone either ease my concerns or give me good reasons why this could cause problems (I couldn't find any issues with initial testing).
The table is fairly small <10 rows total and would require custom row heights even without this hidden row solution.
I do the same thing in the code I just worked on. I am not happy with different behaviour for different table view settings.
The alternative in my case is more complex (a model that adapts to what is visible or not).
For now, I put a //HACK comment on it and document a few peculiarities.
This is what I have found (iOS 5.0 tested):
Set tableView.rowHeight = 1; Zero will give a cell with zero height (as returned by tableView:tableView heightForRowAtIndexPath:) some default height.
You must have a cell separator. If none is selected, then a default height is assigned to zero height rows. The height of 1 is included with the separator.
If your code works in a different way, it would be interesting to know how it is set up.
It would be cleaner to add and remove the rows between two beginUpdates and endUpdates calls, but I don't see why this 0-height method should not work.
If there are no UI-artifacts, that is (e.g. the Delete button showing up overflowing to the next cell).
I use this method of setting hidden cell heights to 0. It works well and also means I can animate the inclusion of new cells by expanding the cell height (such as adding a DatePicker Cell like the calendar app does).
A few things I have had to watch out for in iOS 7.1 are that very squashed text does still appear even when a cell height is = 0 so I've needed to remove cell text in that case. Also, I have change the size of the cell's separatorInset as that was appearing as well.