11

I am trying to create an NSTextField programmatically.

I want to use this NSTextField with auto layout, so its width will be defined automatically to display the entire line (there is only one line of text).

The problem is that textField.intrinsicContentSize and textField.fittingSize are both have -1 and 0 values for the horizontal coordinate, as the output of the code below is: textField.intrinsicContentSize={-1, 21} textField.fittingSize={0, 21}

The code:

NSTextField* textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 10, 24)];    
NSString* str = @"One line of text here.";
[textField setStringValue: str];
[textField setTranslatesAutoresizingMaskIntoConstraints:NO];
[textField invalidateIntrinsicContentSize];

NSLog(@"textField.intrinsicContentSize=%@", NSStringFromSize(textField.intrinsicContentSize));
NSLog(@"textField.fittingSize=%@", NSStringFromSize(textField.fittingSize));

This makes the text field to have a zero width assigned by auto layout.

What should I do to get meaningful values for the fittingSize and intrinsicContentSize properties of the text field so they reflect the content of the text field?

Andriy
  • 2,767
  • 2
  • 21
  • 29
Yoav
  • 5,962
  • 5
  • 39
  • 61
  • 4
    This is solved here: http://stackoverflow.com/q/20809622/210171 (You must set editable = false for the text field to automatically size) – nielsbot Apr 07 '15 at 23:41
  • @nielsbot This only works if you never change the value of the text field. As soon as you programmatically alter the value of the text field, the text field collapses to -1 width. Setting constraints does not alter this. So what are we supposed to do then? This problem does not happen when using a text field that is in Interface Builder, only a text field initialized programmatically. – Sparky Apr 26 '18 at 01:27

3 Answers3

13

Another way to compute the text field optimal width without using fittingSize is using the following code (replacing John Sauer's sizeTextFieldWidthToFit method):

- (void) sizeTextFieldWidthToFit {
    [textField sizeToFit];
    CGFloat newWidth = NSWidth(textField.frame);
    textFieldWidthConstraint.constant = newWidth;
}

You can set the priority of textFieldWidthConstraint to be lower than the priority of another inequality constrain the represent your requirement to a minimal size of 25.

Yoav
  • 5,962
  • 5
  • 39
  • 61
  • This works, but what's the point in using _auto_layout if you need to set the constraint manually? – cutsoy Dec 01 '19 at 21:09
  • 1
    Looks like this is still the way to get it done in 2021 for Big Sur. The intrinsic content size doesn’t seem to work correctly and the frame’s width is only correct after `sizeToFit` until the next layout pass. So it’s important to set the width constraint constant from the frame’s width right after calling `sizeToFit`. – Frank R May 27 '21 at 10:23
3

I can't explain why NSTextField's intrinsicContentSize's widths are -1, but I was able to calculate one based off its attributedString's size. Here's how I created the NSTextField:

// textField is an instance variable.
textField = [NSTextField new] ;
textField.translatesAutoresizingMaskIntoConstraints = NO ;
[view addSubview:textField] ;

NSDictionary* viewsDict = NSDictionaryOfVariableBindings(view, textField) ;
// textFieldWidthConstraint is an instance variable.
textFieldWidthConstraint  = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[textField(==0)]"  options:0  metrics:nil  views:viewsDict] [0] ;
[view addConstraint:textFieldWidthConstraint] ;
NSNumber* intrinsicHeight = @( textField.intrinsicContentSize.height ) ;
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[textField(==intrinsicHeight)]"  options:0  metrics:NSDictionaryOfVariableBindings(intrinsicHeight)  views:viewsDict]] ;

// Position textField however you like.
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[textField]"  options:0  metrics:nil  views:viewsDict]] ;
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[textField]"  options:0  metrics:nil  views:viewsDict]] ;

[self sizeTextFieldWidthToFit] ;
// set textField's delegate so that  sizeTextFieldWidthToFit  is called when textField's text changes.
textField.delegate = self ;

And here are the methods that resize it as its text changes:

- (void) controlTextDidChange:(NSNotification*)aNotification {
    if ( aNotification.object == textField )
        [self sizeTextFieldWidthToFit] ;
}

- (void) sizeTextFieldWidthToFit {
    // I'd like the width to always be >= 25.
    CGFloat newWidth = 25 ;
    if ( ! [textField.stringValue isEqualToString:@""] ) {
        NSDictionary* attributes = [textField.attributedStringValue  attributesAtIndex:0  effectiveRange:nil] ;
        NSSize size = [textField.stringValue sizeWithAttributes:attributes] ;
        newWidth =  MAX(newWidth, size.width + 10) ;
    }
    textFieldWidthConstraint.constant  = newWidth  ;
}
John Sauer
  • 4,411
  • 1
  • 25
  • 33
  • Thanks. For computing the width manually I was able to use [textField sizeToFit] followed by getting the control frame width (which requires a much shorter code). – Yoav Feb 07 '13 at 19:08
  • Could you post that code as an answer? Since it works and is much shorter, it should probably be the accepted answer. – John Sauer Feb 07 '13 at 19:10
  • Sorry, I didn't notice that only three lines in your solution are actually used to compute the optimal width, so eventually my solution is not much shorter, but I posted it anyway as a possible alternative. – Yoav Feb 07 '13 at 20:08
  • It seems to me this answer is just a work around. What I'd like to know is how to have NSTextField intrinsicContentSize work as expected. – pfandrade Dec 28 '13 at 00:19
1

I'm adding this here because these answers are old and I couldn't find everything needed to get NSTextField to automatically wrap and play nice using autolayout and NSConstraints in a single answer.

With these settings the text will:

  • Remain in the horizontal boundaries set by the constraints.
  • Wrap automatically on word boundaries.
  • Automatically adjust its height as required.
  • Push down anything below (if attached by constraints).

This is the code to make it just work:

NSTextField *instructions = [NSTextField labelWithString:@"Some very long text..."];
instructions.translatesAutoresizingMaskIntoConstraints = NO;
instructions.autoresizesSubviews = YES;
instructions.usesSingleLineMode = NO;
instructions.lineBreakMode = NSLineBreakByWordWrapping;
[instructions cell].wraps = YES;
[instructions cell].scrollable = NO;
instructions.maximumNumberOfLines = 10;
instructions.preferredMaxLayoutWidth = 400.0;
[self addSubview:iCloudInstructions];

[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[instructions]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(instructions)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[instructions]-(20)-[button]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(instructions, instructions)]];
Cliff Ribaudo
  • 8,932
  • 2
  • 55
  • 78
  • 1
    Thanks for mentioning `maximumNumberOfLines`, I always thought that this was an UIKit only property – Ely Jul 31 '22 at 12:50