NSTextField width and autolayout - cocoa

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?

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.

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 ;
}

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)]];

Related

Centering text in OS X Screensaver

I'm working on implementing a (very) simple screensaver using the Screensaver framework in OS X 10.10. Positioning on the center of the screen and displaying a two-line text works without problem, but setting the alignment to NSCenterTextAlignment somehow doesn't (the text is always displayed left-aligned).
- (void)animateOneFrame
{
// calculate font size based on screen
NSSize size = [self bounds].size;
CGFloat fontsize = size.height / 11;
// set text
NSMutableParagraphStyle *centredStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[centredStyle setAlignment:NSCenterTextAlignment];
NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"Futura" size:fontsize], NSFontAttributeName,
[NSColor orangeColor], NSForegroundColorAttributeName,
centredStyle, NSParagraphStyleAttributeName,
nil];
NSString *theText = #"Simple text spanning\ntwo lines";
// position text on screen
NSRect boundingRect = [theText boundingRectWithSize:size options:0 attributes:textAttributes];
NSPoint point = NSMakePoint((size.width - boundingRect.size.width) / 2.0,
(size.height - boundingRect.size.height) / 2.0);
[theText drawAtPoint: point withAttributes: textAttributes];
}
Any pointers on how to solve this are appreciated.
PS: I know that I don't need to put everything into animateOneFrame but for the moment the goal is to get it working at all:-)

Updating the size of NSTextField with respect to length of the string

I am trying to update the height of the NSTextField w.r.t the length of the string but if the string is light the last portion gets cut off.This is my code.
self.solutionTextView.frame = CGRectMake(self.solutionTextView.frame.origin.x, self.solutionTextView.frame.origin.y, self.solutionTextView.frame.size.width + 25 ,self.solutionTextView.frame.size.height + 25);
//self.solutionTextView.sizeToFit()
What am i doing wrong?
If the NSTextField is allowed to wrap the string, the following steps are the standard (or usual? or one of more possible?) ways to determine the wanted height of the field. Assume the width of the field is given:
NSRect frame = [textField frame];
Now make it very high:
NSRect constraintBounds = frame;
constraintBounds.size.height = 10000.0;
[textField setFrame: constraintBounds];
and set the string:
[textField setStringValue:theString];
Ask now for the natural size:
NSSize naturalSize =
[[textField cell] cellSizeForBounds:constraintBounds];
And you are done:
frame.size = naturalSize;
[textField setFrame:frame];

Creating right aligned NSView

I want to create a NSView container such that any NSControl object added should be right aligned.
I have added a method to MyCustomNSView class as following. Currently I am adding buttons which are getting left aligned.
- (void) _addButton:(NSString *)title withIdentifier:(NSString *)identifier {
NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(100 * [_buttonIdentifierList count] + 10 , 5, 70, 20)];
[button setTitle:title];
[button setAction:#selector(actionButtonPressed:)];
[button setTarget:self];
[button setIdentifier:identifier];
[self addSubview:button];
[_buttonIdentifierList addObject:identifier];
}
So what modifications do I have to make to the above method so that it will add the objects from right side.
I was planning to do it mathematically(Generating frame origin that would generate right aligned origin point). I also tried out using NSLayoutConstrains but didnt work out..
How do I do it using autolayouts ?
To do it by manual positioning, you would compute the frame for the button something like this:
NSButton *button = [[NSButton alloc] initWithFrame:NSMakeRect(NSMaxX(self.bounds) - (100 * [_buttonIdentifierList count] + 10) - 70, 5, 70, 20)];
That is, you take your current calculation which is an offset toward the right (from the left edge) and negate it to make it an offset toward the left. You add the value of the right edge of the containing view so it's an offset from the right edge. That has computed the X position of the right edge of the button, so you subtract the button's width to get the origin of the button, which is on its left edge.
To use auto layout (which uses NSLayoutConstraint), you could do this:
NSButton *button = [[NSButton alloc] initWithFrame:NSZeroRect];
[button setTitle:title];
[button setAction:#selector(actionButtonPressed:)];
[button setTarget:self];
[button setIdentifier:identifier];
button.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:button];
__block NSButton* previousButton = nil;
if (_buttonIdentifierList.count)
{
NSString* previousButtonIdentifier = _buttonIdentifierList.lastObject;
[self.subviews enumerateObjectsUsingBlock:^(NSView* subview, NSUInteger idx, BOOL *stop){
if ([subview.identifier isEqualToString:previousButtonIdentifier])
{
previousButton = (NSButton*)subview;
*stop = YES;
}
}];
}
NSDictionary* metrics = #{ #"buttonWidth": #70,
#"buttonHeight": #20,
#"buttonSeparation": #30,
#"horizontalMargin": #10,
#"verticalMargin": #5 };
if (previousButton)
{
NSDictionary* views = NSDictionaryOfVariableBindings(button, previousButton);
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[button(buttonWidth)]-(buttonSeparation)-[previousButton]" options:NSLayoutFormatAlignAllBaseline metrics:metrics views:views];
[self addConstraints:constraints];
}
else
{
NSDictionary* views = NSDictionaryOfVariableBindings(button);
NSArray* constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"[button(buttonWidth)]-(horizontalMargin)-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[button(buttonHeight)]-(verticalMargin)-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
}
[_buttonIdentifierList addObject:identifier];
Finding the previousButton would be simplified if you keep track of the buttons, rather than the identifiers. If you have a button object, it's easy to get its identifier, but the reverse (getting the button object when all you have is the identifier) is not as simple.
If you want to allow the buttons to be their natural width and height, rather than a fixed value, you can just leave out those width/height specifiers (that is, use [button] rather than [button(buttonWidth)]). If you want all of the buttons to have the same width, but let the system pick the width of the naturally widest button, you can use [button(==previousButton)]. Since a button's default compression resistance priority is higher than its content hugging priority, it will pick the smallest width that doesn't compress any of them.
If you want the buttons to be the standard distance away from each other, rather than the fixed value of 30 points, you can use use - instead of -(buttonSeparation)-. Similarly, if you want them to be the standard distance from the superview edge, you can use - instead of -(horizontalMargin)- or -(verticalMargin)-.

How to set up a NSLayoutConstraint in a NSScrollView?

I am new to NSLayoutConstraint.
I want to manage the content of a NSScrollView programmatically. At the end, what I want to do is to create a subclass of NSScrollView with a method - add:(NSView *)aView that will add aView after the others and enlarge the documentView of the NSScrollView if necessary.
(all this for OSX) I guess it exists but can't find it, so if anyone knows where to find this, I am in ;) Here is a picture that can help you to understand :
My plan to do this is to use the NSLayoutConstraints. The constraints will be :
equal vertical space between view 1, view 2, etc.
the documentView height should adapt to the sum of the heights of view 1, view 2, etc.
some fixed vertical margins between the documentViewand view 1 and the last view.
view 1, view 2, etc. are right-aligned
So, let us come to my question : I have started by just adding view 1 (I will see it after for the rest). I did the following :
theScrollView is my NSScrollView ;
newSubview is the view I want to add ; with respect to the picture, it is view 1.
theContainer = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)] ;
[theScrollView setDocumentView:theContainer] ;
[newSubview setTranslatesAutoresizingMaskIntoConstraints:NO];
[theContainer addSubview:newSubview] ;
NSLog(#"superview ? %#", [newSubview superview]) ;
NSLog(#"view ? %#", newSubview) ;
NSLog(#"NSScrollView ? %#", self) ;
NSLog(#"NSScrollView content ? %#", self.contentView) ;
NSArray * arrayOfConst1 ;
arrayOfConst1 = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-50-[newSubview]"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(newSubview)];
NSLog(#"First constraints : %#", arrayOfConst1) ;
NSArray * arrayOfConst2 ;
arrayOfConst2 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[newSubview]"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(newSubview)];
NSLog(#"Second constraints : %#", arrayOfConst2) ;
[newSubview removeConstraints:[newSubview constraints]] ;
[theContainer removeConstraints:[theContainer constraints]] ;
[theContainer addConstraints:arrayOfConst1] ;
[theContainer addConstraints:arrayOfConst2] ;
NSLog(#"Frame of theContainer : %#", NSStringFromRect(theContainer.frame));
But, it doesn't work, the frame of theContainer remains (0,0,0,0).
I am new to NSLayoutConstraints ; what I am missing ?

What constant can I use for the default Aqua space in Autolayout?

According to the Cocoa Auto Layout Guide, I can use a dash in the visual constraint format language to "denote the standard Aqua space:"
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"[a]-[b]"
options:0
metrics:nil
views:viewDict]];
However, I can't seem to find an NSLayout... constant or method that allows me to do the same thing if I'm building a constraint without using the visual format language:
[self addConstraint:[NSLayoutConstraint constraintWithItem:a
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:b
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:<# ??? #>]];
Is there a constant (or another value or method) that I can use to define the Aqua space in such a situation?
I've found the "standard Aqua space" to be 8.0 between sibling views, and 20.0 between a view and its superview.
NSView* view = [NSView new] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSiblings = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-[view]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
CGFloat standardConstantBetweenSiblings = constraintWithStandardConstantBetweenSiblings.constant ; // 8.0
NSView* superview = [NSView new] ;
[superview addSubview:view] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSuperview = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
CGFloat standardConstantBetweenSuperview = constraintWithStandardConstantBetweenSuperview.constant ; // 20.0
For iOS users:
#import "NSLayoutConstraint+StandardOffsets.h"
#implementation NSLayoutConstraint (StandardOffsets)
+ (CGFloat)standardConstantBetweenSiblings
{
static CGFloat value;
if(!isnormal(value)) {
UIView *view = [UIView new] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSiblings = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-[view]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
value = constraintWithStandardConstantBetweenSiblings.constant ; // 8.0
}
return value;
}
+ (CGFloat)standardConstantBetweenSuperview
{
static CGFloat value;
if(!isnormal(value)) {
UIView *view = [UIView new] ;
UIView *superview = [UIView new] ;
[superview addSubview:view] ;
NSLayoutConstraint* constraintWithStandardConstantBetweenSuperview = [NSLayoutConstraint constraintsWithVisualFormat:#"[view]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view) ] [0] ;
value = constraintWithStandardConstantBetweenSuperview.constant ; // 20.0
}
return value;
}
#end
PS: I entered a bugreport that no constant is offered in the header files!
Based on John Sauer's answer, I wound up writing a couple methods on an NSLayoutConstraint category to find the constants:
+standardAquaSpaceConstraintFromItem:toItem: returns a single NSLayoutConstraint constructed using the visual format language; it asserts that the array of constraints generated from the format has exactly one item, then gets that item and gives it back.
+standardAquaSpaceFromItem:toItem: pulls the constant out of the constraint from the previous method and returns it as a CGFloat.
This way, I can either get the constant value myself if I need to do computations, or just get a single layout constraint with the right spacing (e.g. for assigning to a constraint #property or directly adding to my view).

Resources