Aligning NSTextField and an Image - cocoa

I am trying to align an NSTextField and an NSImageView.. My current code is below. I have tried a bunch of different approaches including subclassing NSTextFieldCell (found here on SO), messing around with the frame of the text field, and tweaking constraints, but I just can't get it.. No matter what I do, it looks like the screenshot below..
I also have discovered that when I don't apply a font to the label, alignment works as I would expect -- it is vertically aligned with the image.
So the question is, a) why in the world does applying a font screw up the alignment, and b) how do I get around this, ideally in a dynamic way that will adapt if i change the font at runtime..
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
NSView *spacer1 = [[NSView alloc] init];
[spacer1 setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:spacer1];
NSView *spacer2 = [[NSView alloc] init];
[spacer2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:spacer2];
NSImage *icon = [NSImage imageNamed:#"05-arrow-west"];
NSImageView *iconView = [[NSImageView alloc] init];
[iconView setTranslatesAutoresizingMaskIntoConstraints:NO];
[iconView setImage:icon];
[self addSubview:iconView];
NSFont *font = [NSFont fontWithName:#"HelveticaNeue-Light" size:14];
NSTextField *label = [[NSTextField alloc] init];
[label setTranslatesAutoresizingMaskIntoConstraints:NO];
[label setEditable:NO];
[label setSelectable:NO];
[label setBezeled:NO];
[label setDrawsBackground:NO];
[label setFont:font];
[label setTextColor:[NSColor lightGrayColor]];
[label setStringValue:#"Test String"];
[self addSubview:label];
NSDictionary *views = NSDictionaryOfVariableBindings(iconView, label, spacer1, spacer2);
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[spacer1(>=0)][iconView]-5-[label][spacer2(==spacer1)]|"
options: 0
metrics:nil
views:views]];
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[iconView]|"
options: 0
metrics:nil
views:views]];
[self addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[label]|"
options: 0
metrics:nil
views:views]];
[self setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
[iconView setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
[label setContentHuggingPriority:200 forOrientation:NSLayoutConstraintOrientationVertical];
}
return self;
}

This is a very common problem related to AppKit calculating incorrect metrics for certain fonts. Helvetica Neue and its variants are susceptible to this problem. Autolayout depends on the intrinsicContentSize of the NSTextField, which uses broken metrics to calculate the appropriate size to display the text. The only way I know of to work around this problem is to use magic offsets in your layout constraints to manually align the text.

In the end, this did it:
[self addConstraint:[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeBaseline
relatedBy:NSLayoutRelationEqual
toItem:iconView
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0]];

Related

Auto Layout on OS X - make NSTextField fill superview

I can't get Auto Layout to work on OS X. What I'm trying to do is pretty simple, namely I have an NSTextField that is supposed to fill the entire space of its superview. Here's the minimal working example code I'm using:
#import AppKit;
int main() {
#autoreleasepool {
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 300, 300)
styleMask:(NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask)
backing:NSBackingStoreBuffered
defer:NO];
NSTextField *textField = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)];
textField.stringValue = #"Lorem ipsum dolor sit atmet.";
[window.contentView addSubview:textField];
textField.translatesAutoresizingMaskIntoConstraints = NO;
window.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[textField setContentHuggingPriority:1
forOrientation:NSLayoutConstraintOrientationHorizontal];
[textField setContentHuggingPriority:1
forOrientation:NSLayoutConstraintOrientationVertical];
[window.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[textField]-|"
options:0
metrics:nil
views:#{#"textField": textField}]];
[window.contentView addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[textField]-|"
options:0
metrics:nil
views:#{#"textField": textField}]];
window.contentView.wantsLayer = YES;
window.contentView.layer.borderWidth = 5;
[window makeKeyAndOrderFront:nil];
[[NSApplication sharedApplication] run];
[textField release];
[window release];
}
return EXIT_SUCCESS;
}
When I run this, the text field doesn't appear. When you resize the window, you can clearly see the border of the content view resizing properly. What am I doing wrong?
I believe you shouldn't alter NSWindow's contentView behavior, e.g. setting its translatesAutoresizingMaskIntoConstraints property.
Removing the following line works for me:
window.contentView.translatesAutoresizingMaskIntoConstraints = NO;

How to place 2 NSTextView vertically aligned with a space in between, and make container view resize accordingly with auto layout

Hope the title is clear. Trying to have something below
--------------------
| [titleTextView] |
| | |
| [detailsTextView]|
--------------------
With the code that I tried, the container resized, but both titletextView and detailsTextView are placed together (overlapping each others). I know I init both at (16,0) but shouldn't the constrain place them correctly?
I also get the following error: Unable to simultaneously satisfy constraints:
("<NSLayoutConstraint:0x60000008fcd0 NSTextView:0x600000134be0.bottom == NSView:0x600000134c80.bottom + 20>",
"<NSAutoresizingMaskLayoutConstraint:0x608000093ce0 h=--& v=--& V:[NSTextView:0x600000134be0]-(0)-| (Names: '|':NSView:0x600000134c80 )>")
Code:
//title textView
self.titleTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(16, 0, [self.view frame].size.width - 30, 0)];
[self.titleTextView setEditable:NO];
[self.titleTextView setBackgroundColor:[NSColor clearColor]];
[self.titleTextView setString:#"potentially long text."];
[self.titleTextView setHorizontallyResizable:NO];
[self.titleTextView sizeToFit];
//detail textView
self.detailsTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(16, 0, [self.view frame].size.width - 30, 0)];
[self.detailsTextView setEditable:NO];
[self.detailsTextView setBackgroundColor:[NSColor clearColor]];
[self.detailsTextView setString:#"Very long text."];
[self.detailsTextView setHorizontallyResizable:NO];
[self.detailsTextView sizeToFit];
//Adding to self.view
[self.view addSubview: self.titleTextView];
[self.view addSubview: self.detailsTextView];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.titleTextView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.detailsTextView
attribute:NSLayoutAttributeTop
multiplier:1
constant:20]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.titleTextView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1
constant:20]];
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:self.detailsTextView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1
constant:20]];
When working with autolayout, you don't want to think about the layout in terms of frames at all. The constraints will determine the frame of your views.
Also, if you're creating the layout in code, you have to call setTranslatesAutoresizingMaskIntoConstraints:NO for the views which you want the autolayout engine to apply to.
So, you'd want to do something like:
UIView* titleTextView = [[UIView alloc] init];
[titleTextView setTranslatesAutoresizingMaskIntoConstraints:NO];
UIView* detailsTextView = [[UIView alloc] init];
[detailsTextView setTranslatesAutoresizingMaskIntoConstraints:NO];
...additional setup stuff...
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[titleTextView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(titleTextView)]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-[detailsTextView]-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(detailsTextView)]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[titleTextView][detailsTextView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(titleTextView, detailsTextView)]];
If working in code, I'd strongly recommend checking out the visual format language which will make setting up constraints much more efficient in code, but you can also do the same thing as above using individual constraints.
Forget all you've done earlier with rects and start thinking in relative positions.
Using my favorite category for autolayout:
https://github.com/jrturton/UIView-Autolayout
you can achieve what you want with these simple constraints (which I find MUCH more readable and intuitive than any of the official API solutions):
[self.titleTextView pinToSuperviewEdges:JRTViewPinLeftEdge|JRTViewPinTopEdge inset:20.0];
[self.detailsTextView pinToSuperviewEdges:JRTViewPinLeftEdge inset:20.0];
[self.detailsTextView pinEdge:NSLayoutAttributeTop toEdge:NSLayoutAttributeBottom ofView:self.titleTextView inset:20];
This will pin both textviews 20 pixels from the left, titleTextView 20 pixels from the top and detailsTextView 20 pixels below titleTextView. Also, the category will add the constraints to the correct view in each case.

UIScrollView Autolayout prevent from scrolling vertically

I'm wondering how to crop image inside UIscrollView with autolayout
I'm trying to make UIscrollView scroll only horizontally. if image is higher than view height it should be cropped. I've tried a lot properties but can't make all images inside uiscrollview same height as view to avoid scrolling vertically.
Do i miss something?
#import "WelcomeController.h"
#interface WelcomeController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (nonatomic,strong) NSArray *contentList;
#end
#implementation WelcomeController
#synthesize contentList =_contentList;
- (void)updateUI
{
UIScrollView* sv = self.scrollView;
id previousLab = nil;
for (UIView *lab in _contentList) {
lab.translatesAutoresizingMaskIntoConstraints = NO;
lab.backgroundColor = [UIColor blackColor];
lab.contentMode = UIViewContentModeScaleAspectFit;
[sv addSubview:lab];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[lab]|"
options:0 metrics:nil
views:#{#"lab":lab}]];
if (!previousLab) { // first one, pin to top
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[lab]"
options:0 metrics:nil
views:#{#"lab":lab}]];
} else { // all others, pin to previous
[sv addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:#"H:[prev][lab]"
options:0 metrics:nil
views:#{#"lab":lab, #"prev":previousLab}]];
}
previousLab = lab;
}
// last one, pin to bottom and right, this dictates content size height
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[lab]|"
options:0 metrics:nil
views:#{#"lab":previousLab}]];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[lab]|"
options:0 metrics:nil
views:#{#"lab":previousLab}]];
}
-(void)setContentList:(NSArray *)contentList
{
_contentList = contentList;
[self updateUI];
}
- (void)setupScrollView
{
UIScrollView* sv = [UIScrollView new];
sv.backgroundColor = [UIColor redColor];
sv.translatesAutoresizingMaskIntoConstraints = NO;
sv.pagingEnabled = YES;
sv.showsHorizontalScrollIndicator =NO;
sv.showsVerticalScrollIndicator = NO;
sv.bounces =NO;
[self.view addSubview:sv];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[sv]|"
options:0 metrics:nil
views:#{#"sv":sv}]];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[sv]|"
options:0 metrics:nil
views:#{#"sv":sv}]];
self.scrollView = sv;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupScrollView];
//for testing
UIImageView *image1=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"welcome1.jpg"]];
UIImageView *image2=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"welcome2.jpg"]];
UIImageView *image3=[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"welcome3.jpg"]];
self.contentList = [NSArray arrayWithObjects:image1,image2,image3,nil];
}
#end
Have you tried setting the height of the scroll view explicitly?
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_sv(123)]" options:0 metrics:nil views:views]];
You would need to replace the height above (123) with the height you need, obviously, as well as views.
Autolayout automatically manages the UIScrollView's contentSize (the scrollable area). So if you stick a subview in there with an intrinsic size that is larger than the height, it will increase the contentSize. I can think of two things:
Stick images in a plain UIView with the same height as the scrollview.
Subclass the UIImageView and override the intrinsicContentSize method to return a fixed height for all images. This seems like a poor solution, though.
I think you should be able to set the image view's frames to a fixed height (via constraints). Then add constraints to have the image views top and bottom fixed with x constant from the scrollview.
This will let the scrollview know its exact content size to use. As long as its frame, then, (determined by whatever constraints you give it in relation to its superview) is >= the image view's fixed heights, it won't scroll vertically.

How to add fixed width and height constraints between windows content view and NSViewControllers View

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.rootVC = [[VZMConversationViewController alloc] initWithNibName:#"VZMConversationViewController" bundle:[NSBundle mainBundle]];
[self.window.contentView addSubview:self.rootVC.view];
self.rootVC.view.frame = ((NSView*)self.window.contentView).bounds;
/*
[NSLayoutConstraint constraintWithItem:self.rootVC.view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.window.contentView attribute:NSLayoutAttributeWidth multiplier:1 constant:0];
[NSLayoutConstraint constraintWithItem:self.rootVC.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.window.contentView attribute:NSLayoutAttributeHeight multiplier:1 constant:0];
//above 2 lines are not working
*/
}
Hey i am an iPhone developer trying to understand mac development, i have the above code and how to add constraints to first view controllers view so that controllers view resizes with the window. Can this be done in XIB?
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.rootVC = [[VZMConversationViewController alloc] initWithNibName:#"VZMConversationViewController" bundle:[NSBundle mainBundle]];
NSView *rootView=self.rootVC.view;
[rootView setTranslatesAutoresizingMaskIntoConstraints:NO];
[[[self window] contentView] addSubview:rootView];
[[[self window] contentView] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[rootView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(rootView)]];
[[[self window] contentView] addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[rootView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(rootView)]];
}
This fixed the problem

Make NSView with fixed width and variable height that fits contents

I want to create a view with fixed width, but variable height. It means that view should automatically be resized according to its contents height, but, at the same time, it should keep the same width.
How can I achieve that programmatically?
For example, I've got the next piece of code to create a label and a button:
NSTextField *label = [[NSTextField alloc] initWithFrame:[self frame]];
[label setEditable:NO];
[label setBackgroundColor:[NSColor clearColor]];
[label setBezeled:NO];
[label setFont:[NSFont fontWithName:#"Lucida Grande" size:13.0]];
[label setStringValue:#"Sample label text"];
NSButton *button = [[NSButton alloc] initWithFrame:primaryBounds];
[button setBezelStyle:10];
[button setTitle:#"Sample button text"];
[button setBounds:NSInsetRect([button bounds], -8.0, 0)];
[button sizeToFit];
[[self contentView] addSubview:label];
[[self contentView] addSubview:button];
They were set to fill the entire contentView frame. How can I set my label to have fixed width and variable height (based on text contents of itself), and my button to be attached to the bottom of the label?
Okay, I've managed to autosize label like this:
NSTextView *label = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, [self frame].size.width, 0)];
[label setEditable:NO];
[label setBackgroundColor:[NSColor clearColor]];
[label setFont:[NSFont fontWithName:#"Lucida Grande" size:13.0]];
[label setString:#"Sample label text"];
[label setHorizontallyResizable:NO];
[label sizeToFit];
Under Autolayout using Mountain Lion, you would tell the text field what its preferred width should be:
[textField setPreferredMaxLayoutWidth:200]
Now the text field will measure the size of its content as if it wraps at 200 points, and once the content reaches that width, the text field will prefer to grow vertically.
To attach the button to the bottom of the label, you would add a constraint that says the bottom of the label equals the top of the button, plus 22:
[parentView addConstraint:
[NSLayoutConstraint constraintWithItem:label
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:button
attribute:NSLayoutAttributeTop
multiplier:1
constant:22]];
or using the visual format language and standard Aqua spacing:
NSDictionary *viewsDict = NSDictionaryOfVariableBindings(label, button);
[view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:[label]-[button]"
options:0
metrics:nil
views:viewsDict]];

Resources