How to resize NSTextField when content increase in NSTextField - cocoa

i have NSTextField but content that i have to show is not fixed , it will grow dynamically.
is there any way in simple way through which we can change size dynamically.
Thanks in Advance :)

The text field's delegate needs to update the size as the text changes.
The trick is you have to manually update the stringValue property, to avoid bugs:
override func controlTextDidChange(obj: NSNotification) {
guard let field = obj.object as? NSTextField else {
return
}
// re-apply the string value, this is necessary for the text field to correctly calculate it's width
field.stringValue = field.stringValue
// calculate the new dimensions
let maxWidth: CGFloat = 500
let maxHeight: CGFloat = 50
let size = renameField.sizeThatFits(NSSize(width: maxWidth, height: maxHeight))
// apply them
field.frame = NSRect(x: field.frame.origin.x, y: field.frame.origin.y, width: size.width, height: size.height)
}

If you're just showing content, as opposed to typing in the text field, then you should use a label, which can be set in IB to "Size To Fit Content". This will cause the label to be just big enough for the content.

There is no option to set through Interface Builder.
However you can do through codes:
You can set a delegate on the NSTextField which implements
- (void)controlTextDidChange:(NSNotification *)aNotification;
Every time the delegate gets called resize the control per your idea above.
Also, this one.
There is a great category on NSString/NSAttributedString by Jerry Krinock which allows you to calculate the height of text based on its width and vice versa:
http://www.sheepsystems.com/sourceCode/sourceStringGeometrics.html
Edit March 2016 :
Now as Autolayout feature is there for Mac OS applications that helps to increase and decrease the size of textfield and even NSWindow, so you can use them it very easy to use. Just put constraints from either side of the Window, and set the constraints something as ( >= desiredMinimumSize ) and you are done!

Related

Autolayout support for custom text view

This question is about supporting a variable-height, custom text view using constraints and the view's intrinsicContentSize for autolayout. Before you click that 'duplicate' button, hear me out.
I have a custom text view (from scratch, inherits from NSView). It supports many of the usual NSTextView features, the most relevant here being multiple lines and laying out its content based on width available. The app it's built for loads a couple of these text views into each row of a table view. The issue is that the height doesn't get set properly according to its intrinsicContentSize.
I created a sample project that simplifies the problem. It uses a "pseudo" text view with a fixed number and size of characters used to determine width/height required. In the sample, there is a table view of one column whose cell view has only one subview, a PseudoTextView. The text view is pinned to the edges of its cell view with a little padding. How can I get the system to recognize that the text view should abide by the constraints that define the width while allowing the text view to grow in height, wrapped tightly by the cell view? Here's the text view code:
class PseudoTextView: NSView {
#IBInspectable var characterCount: Int = 0
#IBInspectable var characterWidth: CGFloat = 5
#IBInspectable var characterHeight: CGFloat = 8
#IBInspectable var background: NSColor = .blue {
didSet {
layer?.backgroundColor = background.cgColor
}
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
wantsLayer = true
layer?.backgroundColor = background.cgColor
}
override var intrinsicContentSize: NSSize {
let requiredWidth = characterWidth * CGFloat(characterCount)
let lineCount = (requiredWidth / frame.width).rounded(.up)
let usedHeight = lineCount * characterHeight
let charactersPerLine = (frame.width / characterWidth).rounded(.down)
let usedWidth = charactersPerLine * characterWidth
return NSSize(width: usedWidth, height: usedHeight)
}
This version returns the appropriate size based on the frame of the view. This obviously doesn't work because it's accessed during the updateConstraints phase of layout when the frame hasn't been set. I've also tried using NSView.noIntrinsicMetric for the width, but this will drive the text view to zero width and the height never recovers. There are an enormous number of other attempts I've made, but I won't bore you with them all.
NSTextField does something different (assuming 'Editable' is off, 'Wrap' is on). It's intrinsicContentSize reports the full width of the text on a single line (even if it's much longer than the width available), but it is somehow resized to the correct width. Once resized, the intrinsicContentWidth then still reports the full single-line width, but the height is adjusted to account for multiple lines. There is some magic somewhere I haven't been able to divine.
I've read every line of related documentation. If there's a blog post on the topic, I've probably read it. If there's a question on SO on the topic, I've probably read it. If you wrote a book on the topic, I've probably bought it. All of these sources tease at the problem I'm having, but none of them answer the question of how to handle this particular situation. Desperate.
Update:
After reading an old blog post by Jonathon Mah (http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html) I created an example that uses his approach. Here's another project that mimics his technique and works correctly. This is the top portion of the app. It's a fixed container view that's adjusted with a slider. The patchwork are the pseudo characters of the custom view whose background is the pink color.
However, when inserted into a self-sizing table view, the custom view correctly matches the width of its cell, but the cell is not adjusted to respect the intrinsic height. If you change the custom view's bottom constraint to be optional (say, with a >= relation) the custom view does shrink to the correct height, but the cell view remains fixed. How do I convince the cell view to shrink its height to respect the intrinsicContentSize.height of its subview?
I have a solution for your problem, although it may not be optimal, since I do not have too much experience with macos specifics. So, first of all, let's define that the table view should use automatic row heights:
override func awakeFromNib() {
super.awakeFromNib()
tableView.usesAutomaticRowHeights = true
}
In your second sample the tableView outlet was not not connected to TableViewController, but it probably should be, so do not forget to connect it.
Now, in your WrappingCellView, you override layout(), but the value that you set for preferredMaxLayoutWidth is incorrect. I think it should be the width of the superview:
override func layout() {
// 16 is used for the sake of example
wrappingView.preferredMaxLayoutWidth = (superview?.frame.width ?? 16) - 16
super.layout()
}
Next part is the one I am not sure about:
func tableViewColumnDidResize(_ notification: Notification) {
tableView.reloadData()
}
There should be a better API to recalculate row heights. I hope you or someone else can suggest something :)
These three adjustments result in proper recalculation of the cell height:

Custom Xib With Defined Frame Constraints Is Changing At Runtime

I've spent several hours googling and in the debugger and I cannot figure out why my parent view in this xib is changing at runtime.
I have created a simple Xib:
In the container I set the width and height constraints (I tried setting constraints in the top parent but I can't seem to be able to):
At runtime I load the Xib programatically and I add it to a view. However after I add it to the view and set the position, the frame of the parent is smaller and the position is wrong.
Here I am explicitly set the x to 16, and the y to 400. When I look at it in the inspector debug tool however, I get different results than I want because the parent frame has changed and the Container position is wrong as a result. I turned the inspector to the side so you can see the parent (in blue) and the child container (in white) and how the parent is smaller than the child:
The details for the parent (the root xib view 'Item Detail Size Widget') are as follows. Notice the height is now 32 instead of 76:
The details for the top level child (Container) are as follows:
So the constraints I set for the container are being honored but the parent is resizing (I assumed since I couldn't set constraints it would use the frame I set).
I tried turning off and on translatesAutoResizingMaskIntoConstraints and a few other things but I can't seem to get the parent to be the exact same size as the Container.
Do you have any suggestions as to why the root Item Detail Size Widget will not match the size of the Container and is changing at run time?
Update
For reference here is the code where I add the widget:
let sizer: ItemDetailSizeWidget = .fromNib()
sizer.x = 16
sizer.y = 400
contentView.addSubview(sizer)
I have UIView extensions that set x and y as follows:
var x: CGFloat {
set { self.frame = CGRect(x: newValue,
y: self.y,
width: self.width,
height: self.height)
}
get { return self.frame.origin.x }
}
var y: CGFloat {
set { self.frame = CGRect(x: self.x,
y: newValue,
width: self.width,
height: self.height)
}
get { return self.frame.origin.y }
}
And here is the fromNib extension
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
Xcode always uses the autoresizing mask for a top-level view in a xib. You can see that your first screen shot: it has the autoresizing control shown.
Your top-level view's autoresizingMask is set to flexible width and height.
You have not set any width or height constraints between your top-level view and the “Container” subview.
You also have this code:
contentView.addSubview(sizer)
I suspect (since you mention contentView) that you're adding sizer to the view hierarchy of a table view cell or a collection view cell. When the cell is first created, it might not yet be at its final size. The collection view (or table view) might resize the cell after you return it from collectionView:cellForRowAtIndexPath:.
Since your sizer view has translatesAutoresizingMaskIntoConstraints set to true when it's loaded from the nib, and since its autoresizingMask is [.flexibleWidth, .flexibleHeight], this means that it will grow or shrink when its superview grows or shrinks.
To fix, try these steps:
Change the autoresizing mask to this:
Constrain the width of “Item Size Detail Widget” to equal the width of “Container”, and constrain the heights to equal also:

Layout subviews in TableViewCell without adding to NSTableView

I have a subclass of a NSTableViewCell that has several controls in it, and I need the width of one of them to calculate the height for the row.
Since its not allowed to get the cell from tableView:heightOfRow, I need to calculate the height taking into account the width of a textField thats inside the cell and the string that goes in that row.
I figured I'd create a single static cell, give it a frame with the current width and make it calculate and perform its layout and get the resulting width for a textField width.
Here is the code thats supposed to get me the width of the "nameTextField"
+(float)nameTextFieldWidthForTaskTableType:(TaskTableType )type compactSize:(BOOL)compactSize totalCellWidth:(float)totalWidth{
static TaskCellView *taskCell = nil;
if (taskCell == nil) {
taskCell = [[TaskCellView alloc]initWithCompactSize:compactSize taskTableType:type identifier:#"notUsed"];
}
if (taskCell.frame.size.width != totalWidth) {
taskCell.frame = NSMakeRect(0, 0, totalWidth, 25);
[taskCell updateConstraints];
}
NSTextField *nameTextField = [taskCell viewForComponent:kTaskCellComponentName];
return nameTextField.frame.size.width;
}
The cell subviews are constraint based. And they work ok. But here, the nameTextField frame always is zero. Does the cell need a superview to do its layout ? Or what else could be missing ?
This did the trick:
[taskCell layoutSubtreeIfNeeded];

Can NSCollectionView autoresize the width of its subviews to display one column

I have an NSCollectionView that contains a collection of CustomViews. Initially it tiled the subviews into columns and rows like a grid. I then set the Columns property in IB to 1, so now it just displays them one after another in rows. However, even though my CustomView is 400px wide, it's set to autoresize, the NSCollectionView is 400px wide, and it's set to 1 column, the subviews are drawn about 80px wide.
I know I can get around this by calling:
CGFloat width = collectionView.bounds.size.width;
NSSize size = NSMakeSize(width, 85);
[collectionView setMinItemSize:size];
[collectionView setMaxItemSize:size];
But putting this code in the awakeFromNib method of my WindowController only sets the correct width when the program launches. When I resize the window (and the NSCollectionView autoresizes as I've specified), the CustomViews stay at their initially set width.
I'm happy to take care of resizing the subviews myself if need be, but I'm quite new to Cocoa and can't seem to find any articles explaining how to do such a thing. Can someone point me in the right direction?
Anthony
The true answer is to set the maxItemSize to 0,0(NSZeroSize). Otherwise, it is computed.
[self.collectionView setMaxItemSize:NSZeroSize];
This can be set in awakeFromNib.
I couldn't get this to work with a default layout - but it is fairly easy to implement a custom layout:
/// Simple single column layout, assuming only one section
class SingleColumnLayout: NSCollectionViewLayout {
/// Height of each view in the collection
var height:CGFloat = 100
/// Padding is wrapped round each item, with double an extra bottom padding above the top item, and an extra top padding beneath the bottom
var padding = EdgeInsets.init(top: 5, left: 10, bottom: 5, right: 10)
var itemCount:Int {
guard let collectionView = collectionView else {
return 0
}
return collectionView.numberOfItems(inSection:0)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
return true
}
override open func layoutAttributesForItem(at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes? {
let attributes = NSCollectionViewLayoutAttributes(forItemWith: indexPath)
guard let collectionView = collectionView else {
return attributes
}
let bounds = collectionView.bounds
let itemHeightWithPadding = height + padding.top + padding.bottom
let row = indexPath.item
attributes.frame = NSRect(x: padding.left, y: itemHeightWithPadding * CGFloat(row) + padding.top + padding.bottom , width: bounds.width - padding.left - padding.right , height: height)
attributes.zIndex = row
return attributes
}
//If you have lots of items, then you should probably do a 'proper' implementation here
override open func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
var attributes = [NSCollectionViewLayoutAttributes]()
if (itemCount>0){
for index in 0...(itemCount-1) {
if let attribute = layoutAttributesForItem(at: NSIndexPath(forItem: index, inSection: 0) as IndexPath) {
attributes.append(attribute)
}
}
}
return attributes
}
override open var collectionViewContentSize: NSSize {
guard let collectionView = collectionView else {
return NSSize.zero
}
let itemHeightWithPadding = height + padding.top + padding.bottom
return NSSize.init(width: collectionView.bounds.width, height: CGFloat(itemCount) * itemHeightWithPadding + padding.top + padding.bottom )
}
}
then all you need is
var layout=SingleColumnLayout()
collectionView.collectionViewLayout = layout
I know this is a very late response but I got the same problem and hope my solution will help somebody. Solution is to access bounds of enclosing scroll view not of collection view itself. So to solve it you need to replace first line with:
CGFloat width = collectionView.enclosingScrollView.bounds.size.width;
another late one - I just switched to using an NSTableView and providing an NSView by the delegate method.
Autoresizing comes for free, one column is easy, and it renders massively faster.
Lets say you want your CollectionViewItem with a size of 200x180px, then you should set:
[myCollectionView setMinItemSize:NSMakeSize(200, 180)];
[myCollectionView setMaxItemSize:NSMakeSize(280, 250)];
Your Max-Size should be big enough to look good and give enough space for stretching to fit the collectionView-Width.
If you have a fixed number of columns, you can probably use (0,0), but if you want a dynamic number of rows and columns like I wanted.. you should set a fixed min-size and a bigger max.size.
While you might get a collection view to behave as you want, I think you have a design problem
You should use a NSTableView and set columns to 1 and their sizing to anything but "None". NSTableView is intended for tabular data, plus it can recycle cells which gives a great performance boost for large amount of items. NSCollectionView is more like a linear layout which arranges items in a grid, with vertical scrolling. It is useful when the column number can change dynamically to allow more items to be shown on screen, usually depending on device orientation and screen size.
I tried all of solutions proposed here and none of them helped. If you use flow layout (it's used by default) you can extend it with the following code and delegate's sizeForItem method will be called on each change
class MyCollectionViewFlowLayout: NSCollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
return true
}
override func invalidationContext(forBoundsChange newBounds: NSRect) -> NSCollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! NSCollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = true
return context
}
}
Hope it helps someone as it took me couple of evenings to find solution
Matt Gallagher's wonderful blog Cocoa with Love is about to address this. This week, he shared the bindings details of a one-column view like the one in question:
http://cocoawithlove.com/2010/03/designing-view-with-bindings.html
Next entry, he promises to share the rest of the implementation, which should give you what you're looking for.
(I should note that he is subclassing NSView directly and re-implementing many of NSCollectionView's features. This seems to be common though; not many people are fond of subclassing NSCollectionView.)
Edit: Seems he broke his promise... we never did receive that post. See tofutim's answer below instead.

How to get height for NSAttributedString at a fixed width

I want to do some drawing of NSAttributedStrings in fixed-width boxes, but am having trouble calculating the right height they'll take up when drawn. So far, I've tried:
Calling - (NSSize) size, but the results are useless (for this purpose), as they'll give whatever width the string desires.
Calling - (void)drawWithRect:(NSRect)rect options:(NSStringDrawingOptions)options with a rect shaped to the width I want and NSStringDrawingUsesLineFragmentOrigin in the options, exactly as I'm using in my drawing. The results are ... difficult to understand; certainly not what I'm looking for. (As is pointed out in a number of places, including this Cocoa-Dev thread).
Creating a temporary NSTextView and doing:
[[tmpView textStorage] setAttributedString:aString];
[tmpView setHorizontallyResizable:NO];
[tmpView sizeToFit];
When I query the frame of tmpView, the width is still as desired, and the height is often correct ... until I get to longer strings, when it's often half the size that's required. (There doesn't seem to be a max size being hit: one frame will be 273.0 high (about 300 too short), the other will be 478.0 (only 60-ish too short)).
I'd appreciate any pointers, if anyone else has managed this.
-[NSAttributedString boundingRectWithSize:options:]
You can specify NSStringDrawingUsesDeviceMetrics to get union of all glyph bounds.
Unlike -[NSAttributedString size], the returned NSRect represents the dimensions of the area that would change if the string is drawn.
As #Bryan comments, boundingRectWithSize:options: is deprecated (not recommended) in OS X 10.11 and later. This is because string styling is now dynamic depending on the context.
For OS X 10.11 and later, see Apple's Calculating Text Height developer documentation.
The answer is to use
- (void)drawWithRect:(NSRect)rect options:(NSStringDrawingOptions)options
but the rect you pass in should have 0.0 in the dimension you want to be unlimited (which, er, makes perfect sense). Example here.
I have a complex attributed string with multiple fonts and got incorrect results with a few of the above answers that I tried first. Using a UITextView gave me the correct height, but was too slow for my use case (sizing collection cells). I wrote swift code using the same general approach described in the Apple doc referenced previously and described by Erik. This gave me correct results with must faster execution than having a UITextView do the calculation.
private func heightForString(_ str : NSAttributedString, width : CGFloat) -> CGFloat {
let ts = NSTextStorage(attributedString: str)
let size = CGSize(width:width, height:CGFloat.greatestFiniteMagnitude)
let tc = NSTextContainer(size: size)
tc.lineFragmentPadding = 0.0
let lm = NSLayoutManager()
lm.addTextContainer(tc)
ts.addLayoutManager(lm)
lm.glyphRange(forBoundingRect: CGRect(origin: .zero, size: size), in: tc)
let rect = lm.usedRect(for: tc)
return rect.integral.size.height
}
You might be interested in Jerry Krinock's great (OS X only) NS(Attributed)String+Geometrics category, which is designed to do all sorts of string measurement, including what you're looking for.
On OS X 10.11+, the following method works for me (from Apple's Calculating Text Height document)
- (CGFloat)heightForString:(NSAttributedString *)myString atWidth:(float)myWidth
{
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:myString];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:
NSMakeSize(myWidth, FLT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
[layoutManager glyphRangeForTextContainer:textContainer];
return [layoutManager
usedRectForTextContainer:textContainer].size.height;
}
Swift 4.2
let attributedString = self.textView.attributedText
let rect = attributedString?.boundingRect(with: CGSize(width: self.textView.frame.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
print("attributedString Height = ",rect?.height)
Swift 3:
let attributedStringToMeasure = NSAttributedString(string: textView.text, attributes: [
NSFontAttributeName: UIFont(name: "GothamPro-Light", size: 15)!,
NSForegroundColorAttributeName: ClickUpConstants.defaultBlackColor
])
let placeholderTextView = UITextView(frame: CGRect(x: 0, y: 0, width: widthOfActualTextView, height: 10))
placeholderTextView.attributedText = attributedStringToMeasure
let size: CGSize = placeholderTextView.sizeThatFits(CGSize(width: widthOfActualTextView, height: CGFloat.greatestFiniteMagnitude))
height = size.height
This answer works great for me, unlike the other ones which were giving me incorrect heights for larger strings.
If you want to do this with regular text instead of attributed text, do the following:
let placeholderTextView = UITextView(frame: CGRect(x: 0, y: 0, width: ClickUpConstants.screenWidth - 30.0, height: 10))
placeholderTextView.text = "Some text"
let size: CGSize = placeholderTextView.sizeThatFits(CGSize(width: widthOfActualTextView, height: CGFloat.greatestFiniteMagnitude))
height = size.height
I just wasted a bunch of time on this, so I'm providing an additional answer to save others in the future. Graham's answer is 90% correct, but it's missing one key piece:
To obtain accurate results with -boundingRectWithSize:options: you MUST pass the following options:
NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesDeviceMetrics|NSStringDrawingUsesFontLeading
If you omit the lineFragmentOrigin one, you'll get nonsense back; the returned rect will be a single line high and won't at all respect the size you pass into the method.
Why this is so complicated and so poorly documented is beyond me. But there you have it. Pass those options and it'll work perfectly (on OS X at least).
Use NSAttributedString method
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context
The size is the constraint on the area, the calculated area width is restricted to the specified width whereas the height is flexible based on this width. One can specify nil for context if that's not available. To get multi-line text size, use NSStringDrawingUsesLineFragmentOrigin for options.
As lots of guys mentioned above, and base on my test.
I use open func boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], context: NSStringDrawingContext?) -> CGRect on iOS like this bellow:
let rect = attributedTitle.boundingRect(with: CGSize(width:200, height:0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)
Here the 200 is the fixed width as your expected, height I give it 0 since I think it's better to kind tell API height is unlimited.
Option is not so important here,I have try .usesLineFragmentOrigin or .usesLineFragmentOrigin.union(.usesFontLeading) or .usesLineFragmentOrigin.union(.usesFontLeading).union(.usesDeviceMetrics), it give same result.
And the result is expected as my though.
Thanks.
Not a single answer on this page worked for me, nor did that ancient old Objective-C code from Apple's documentation. What I finally did get to work for a UITextView is first setting its text or attributedText property on it and then calculating the size needed like this:
let size = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.max))
Works perfectly. Booyah!
I found helper class to find height and width of attributedText (Tested code)
https://gist.github.com/azimin/aa1a79aefa1cec031152fa63401d2292
Add above file in your project
How to use
let attribString = AZTextFrameAttributes(attributedString: lbl.attributedText!)
let width : CGFloat = attribString.calculatedTextWidth()
print("width is :: >> \(width)")

Resources