NSTextField label: dynamically change content and grow width to fit - swift

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.

Related

NSTextView in NSOutlineView with IntrinsicContentSize setting wrong height

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!

How to set margin for cell in tableView?

I tried to set a margin for a certain cell in my TableView by using code as below:
cell.layoutMargin.left = 20
However, when I launched the application, it changes nothing on the appearance. Is there any way I could achieve this?
For margins to take effect in any UIView, any constraints must have the "Constrain to margins" checked. Otherwise, the margins will not change any subview's constraints on the superview.
If you are adding constraints with Swift, here is an example of adding constraints relative to margins from Apple. The key part is:
// Get the superview's layout
let margins = view.layoutMarginsGuide
// Pin the leading edge of myView to the margin's leading edge
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
Ensure that your view is being updated and that any changes you are making to UIView is from the main thread.
To give a more thorough answer, more data is needed on your current auto layout setup.
I found a solution or alternative for this problem after getting help from my colleague, what we did was creating an IBOutlet for the constraint (which I just discover). So in UITableviewCell class, i add this line of code :
#IBOutlet var boxLeadingContraint: NSLayoutConstraint!
And in UITableView I set the margin for the specified cell like this:
cell.boxLeadingContraint.constant = 10
Note: I did this on a UIImageView inside the cell not on the cell itself. Any feedback or advice will be appreciated.

Truncate text in label

I am trying to use "Truncate Middle" on a single line label. I checked "Uses Single Line Mode", set Layout to "Truncates" and Line Break to "Truncate Middle" and I've set the Preferred width of the text field to "First Runtime Layout Width" and Content Hugging Priority to 250.
Here's my layout with the <Title> label on which I am trying to enable the truncation:
In my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
titleLabel.stringValue = "This is a very very long title that should be truncated because it would enlarge the window unnecessary!"
}
It does not work, the window is enlarged to show the complete title and the user can't reduce its width.
Strangely, if I change the layout and line break from "Truncates"/"Truncate Middle" to "Wraps"/"Character Wrap", it works fine, but without the middle truncation.
I am expecting truncate to work like this:
This is a very very long text...rge the window unnecessary!
Maybe I'm missing something?
EDIT: Here's the sample project to test the issue: https://nofile.io/f/9s9OJXHaaw1/TestTruncate.zip
If you use Auto Layout for your NSTextView and surrounding NSView you should set your Content Compression Resistance Priority of the NSTextView less than NSLayoutPriorityWindowSizeStayPut (e.g. 499). Otherwise your NSTextView will not truncate its content. (Reference)
I was able to achieve this by setting Content Compression Resistance Priority of Horizontal from 500 to 499.
You can do so by selecting your label -> Size Inspector -> Content Compression Resistance Priority
Giving me the following output:
This is something very simple. Just add this line in override func viewDidLoad()
yourLabel.lineBreakMode = .byTruncatingMiddle
Or you can also set it from Interface Builder. Select your label and set Line Break as Truncate Middle.
For truncate middle, you have to set the alignment to centre as well.

Why will my label not change location when trying to add additional text field?

So I have a button on my storyboard. When that button is pressed, I want to move a certain label down a little bit and also add a new text field to the screen. Here is the simplified code:
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.addIngredientLabel.center.y += self.addIngredientLabel.bounds.height
}) { (_) -> Void in
let newIngredientTextField = UITextField(frame: CGRectMake(100, 110, 100, 100))
newIngredientTextField.center.y -= 50
newIngredientTextField.placeholder = "place text here"
newIngredientTextField.contentHorizontalAlignment = .Left
self.view.addSubview(newIngredientTextField)
}
The problem though, is that after the animation is completed, the label jumps back to its original location. It is like the animation is being pre-maturely terminated. The textField gets added and the label moves but it does not stay moved.
I have tried adding the text field at the end of the function instead of in a completion block. No kind of reordering seems to be helping.
I feel like I am not understanding something fundamental to animations. If anyone has any advice I would greatly appreciate it.
Since you're using the storyboard, you are most likely also using auto-layout. The positions of your controls are determined in your visual design (storyboard) and will adjust automatically to orientation changes and adapt to various device screen sizes. This is good but the flip size is that you cannot directly change the positions of your control using their x,y coordinates.
Instead, you can change values in the constraints you defined in the storyboard. for example, if you button has a "Top" distancs constraint with a control above it (or the edge of the view), you can change the constraint's "constant" attribute to indirectly change the position of the control.
In order to manipulate the contraints in your code, you can create IBOutlets for them just as you would do with any control.

UITableViewCell content moving around

I have an iPhone app in which I'm using a UITableView to format a set of text responses, one per row. I've set up the cell in the storyboard to have a label inside it, and set up a constraint saying that the label should be 10 points from the edge of the cell. I then set up a custom subclass of UITableViewCell, set the cell in the storyboard to be of that class, and connected the outlet.
However, when I load the table, I see the text in the cell moving slightly to the right under some circumstances: when I select the cell, or when I load additional cells into the table. In fact, in the latter case, sometimes everything gets shifted to the right, even cells which were already there!
What the heck is going on here? The only changes I'm making in tableView:cellForRowAtIndexPath: are to the text in the label, and I'm always setting it. And I've unset "Indent While Editing" on the cell in the storyboard.
Answering some of the questions: I'm setting the view up using the storyboard. Xcode isn't reporting any ambiguity with the constraints. Also, here are the screenshots, before and after:
My guess is that the constraints for the label are ambiguous. Ambiguity can make UI components jump around for inexplicable reasons. You probably need to set more constraints for the label to define its position on both axes.
Or, maybe all you need to do is set the label to the "size that fits content" (intrinsic content size) under the Editor menu in IB.
Did you add a new label to the UITableViewCell, or are you working with the textLabel that already exists in it? If you added a new one, consider removing it and using the cell's existing textLabel property instead. If that's not an option for some reason, double-check that the label you've added is in the contentView of the cell, and that all the constraints are relative to the parent view, not to the cell itself.
Also, for debugging, you could set the cell's contentView background color to red (cell.contentView.backgroundColor = [UIColor redColor];) - this might give you a better sense of what's moving, the label or the whole view.
This is just a guess without seeing your code. I had a similar problem once, when the app reused an existing cell, the dimensions of the label were not correct. So, I had to remove the old label in my cellForRowAtIndexPath method, before adding a new label. Here's how I removed the old one:
UIView *oldLabel = [cell viewWithTag:3];
if (oldLabel != nil)
{
[oldLabel removeFromSuperview];
}
Then I added a new label like this:
[cell.contentView addSubview:newLabelOrWhatever];
It might be worth checking to see that your string content doesn't have a space prefixed on it. Also, you could verify the actual position of the label by setting the background color.
I had a similar issue when the label would move when the cell was selected. It was a custom cell that I was loading from a custom Nib.
In the Nib I had not set the backgroundView of the UITableViewCell (superclass) to any view. Once I set it (I set it to the ContentView) the issue stopped.
My auto-layout constrains seems fine and had no issues, so I'm assuming it was the above that fixed it.
If you have a custom subclass of UITableViewCell, try implementing layoutSubviews. Let me try from the top of my head:
static CGFloat const kLeftMargin = 10.0f;
- (void) layoutSubviews
{
[super layoutSubviews];
/* Prepare for processing */
CGSize cellSize = self.bounds.size;
/* Change the textLabel frame */
{
/* Compute the new label frame */
CGSize labelSize = self.textLabel.bounds.size;
CGFloat xCoor = kLeftMargin;
CGFloat yCoor = roundf((cellSize.height - labelSize.height) / 2.0f);
CGRect labelFrame = CGRectMake(xCoor, yCoor,
labelSize.width, labelSize.height);
/* Set the new label frame */
self.textLabel.frame = labelFrame;
}
}
Now, this isn't what is usually recommended (since a lot of people use Storyboards and NIBs), but from experience, if you want some correct layouting done, implement layoutSubviews.