Recently I've been trying to convert an application to use CoreLayout and one of the problems I ran into was with getting content to correctly fill an NSScrollView.
I have an NSView subclass which lays out its subviews in a vertical list using constraints and while the height of the layout is obviously dependent on the combined height of the subviews (plus spacing) the children don't have any intrinsic width and so are dependent on the width the parent class gave them. Ultimately, this was the width which the NSScrollView gives, but the problem was that the NSScrollView wanted to give them 0 width.
The solution I came up with is to add a constraint to the NSClipView which sits inbetween the NSScrollView and my layout view like so:
[_layout setTranslatesAutoresizingMaskIntoConstraints:NO];
[_scrollView setDocumentView:_layout];
NSClipView *clipView = [_scrollView contentView];
NSDictionary *viewsDict = @{@"layout":_layout};
// Add our new constraint that will fix the sides of _layout
// to the sides of its parent, which in this case is clipView
[clipView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[layout]|" options:0 metrics:nil views:viewsDict]];
It was surprisingly easy once I realised that I could add constraints like this. Hopefully this will help some people if they run into issues like this.
Showing posts with label AutoLayout. Show all posts
Showing posts with label AutoLayout. Show all posts
Wednesday, 2 January 2013
Saturday, 13 August 2011
Laying out interfaces automatically with Corelayout Part 2 - Layout Format Language
In the last post on Corelayout we looked at how you can set up autolayout with the Interface Builder. There are times, however, when the layout needs to be done by hand in the code and in Cocoa there is a layout format language that aims to simplify the process of creating constraints.
This language looks something like "|-20-[button]-20-|". The superview is marked by the pipe symbol '|' and this string tells the Corelayout system that a control called button is placed 20 pixels away from the left and right edges of the superview. To set the vertical layout, the layout string should start with "v:". More complicated constraints can be added to the string as well by adding the rules inside parentheses: "|-20-[button(<=350)]-20-|" will restrict the button width to <= 350 pixels.
Lets make a simple example. First, we'll need a function to create a button
This language looks something like "|-20-[button]-20-|". The superview is marked by the pipe symbol '|' and this string tells the Corelayout system that a control called button is placed 20 pixels away from the left and right edges of the superview. To set the vertical layout, the layout string should start with "v:". More complicated constraints can be added to the string as well by adding the rules inside parentheses: "|-20-[button(<=350)]-20-|" will restrict the button width to <= 350 pixels.
Lets make a simple example. First, we'll need a function to create a button
- (NSView *)buttonWithLabel:(NSString *)title
{
NSButton *button = [[[NSButtonalloc] init] autorelease];
[button setBezelStyle:NSRoundedBezelStyle];
[button setTitle:title];
[button setTranslatesAutoresizingMaskIntoConstraints:NO];
return button;
}
Notice that we do not specify a size for the button via initWithFrame: we're just going to leave the button to figure out its own size for itself. This is called its 'intrinsic size'. For a button, its intrinsic height is just enough to display it's child control, you don't normally want a button to be higher than its child control needs to be so we say that it "strongly hugs' it's content vertically, but the intrinsic width of a button can really be anything, so long as it is larger than its content width. In this case we say that it "weakly hugs" it's content horizontally.
The other thing to notice is the call to setTranslatesAutoresizingMaskIntoConstraints:. This call tells Cocoa to ignore the autoresizing mask when working out the constraints as the autoresizing mask may produce a conflicting constraint and we're going to be doing all the constraints ourselves.
As this is a very simple example, the rest of the code is going to go into the applicationDidFinishLaunching: method of the application delegate and it will set up a simple window with 2 buttons in it.
First, we create the buttons and add them to the parent window
NSView *button = [self buttonWithLabel:@"Test button"];
NSView *button2 = [self buttonWithLabel:@"Hello button"];
NSView *view = [window contentView];
[view addSubview:button];
[view addSubview:button2];
The constraints system takes a dictionary so that it can link controls named in the format string to controls. The names in the format string are used as the keys in this dictionary and the function NSDictionaryOfVariableBindings is useful here. It takes a list of objects and creates a dictionary with those objects with the variable names as keys.
NSDictionary *views = NSDictionaryOfVariableBindings(button, button2);
will create a dictionary with the key @"button" that points to the object button, and a key @"button2" which points to the object button2.
Finally we just need to add the constraints to the superview:
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-20-[button]-[button2]-20-|"
options:NSLayoutFormatAlignAllBaseline
metrics:nil
views:views]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button]-55-|"
options:0
metrics:nil
views:views]];
The first constraint sets the horizontal layout, and it says that the control called 'button' is to be placed 20 pixels away from the left hand edge of the superview (which in this case is the contentview of the window), and there should be spacing the size of the default Cocoa spacing between it and 'button2', which should in turn be placed 20 pixels away from the right hand edge of the superview. The options parameter says that all the controls should be aligned along their baselines. The final two parameters; metrics and views, are dictionaries that were mentioned above, to allow the layout system to match values in the format string to real controls. We will look at what the metrics parameter does later on.
The next constraint is the vertical one, and it says that 'button' should be placed the Cocoa default spacing away from the top edge of the window, and 55 pixels away from the bottom edge. We don't need to provide any contraints for button2's vertical placement as it has been aligned with the baseline of 'button'.
And that is all that you need to layout a simple window with resizing controls, next we'll look at some more complicated layout strings.
As in the example in the first article, when you resize the window one of the buttons resizes while the other remains fixed in width. To fix this we need to add an equal width constraint to the format string. If you change the format string for the horizontal constraint to "|-20-[button(==button2)]-[button2]-20-|" and recompile it, you will see that when you expand the window, the two button widths match. And, as in the first article, we can control that the first button will stop expanding at 350pixels, with the second button continuing to expand by setting another constraint to the format string and setting the priority: "|-20-[button(==button2@20,<=350)]-[button2]-20-|"
So, as you can see, we can add as many constraints as we need, separated by commas, in parentheses after the identifier. We can also add constraints to the spacings. For an example, lets make the vertical layout have an expandable space between 20 and 50pixels in size before the buttons. To do this, set the vertical constraint layout to:
"V:|-(>=20,<=50)-[button]-55-|"
We skipped over the metrics parameter earlier, but it works similarly to the views parameter. If you pass in a dictionary that maps a metric name to an NSNumber then that name can be used in place of a number. If we created a metrics dictionary like so:
NSNumber *topHeight = [NSNumber numberWithFloat:55.0];
NSDictionary *metrics = NSDictionaryOfVariableBindings(topHeight);
and passed it into the constraintsWithVisualFormat:options:metrics:views method then we can write a format string that refers to topHeight: "V:|-topHeight-[button]-55-|". When the format string is parsed, the key topHeight will be looked up in the metrics dictionary, and the NSNumber will be used instead. As NSNumbers are immutable, it isn't possible to change what a metric represents.
All in all though, the visual format language seems like an interesting way to describe the layout of an interface, and will certainly make laying out interfaces by hand easier for me at least, as I never really liked having to sit down and work out the frame sizes for all the controls before writing the code. I'm sure I've not covered everything, and there's more to discover about Corelayout as I go.
Apple's Auto Layout guide: http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AutolayoutPG/Articles/Introduction.html
Other articles about Corelayout
Labels:
AutoLayout,
Cocoa,
CoreLayout,
OSX
Sunday, 7 August 2011
Laying out interfaces automatically with Corelayout Part 1
I come from a GTK background on Linux, so I was surprised to find that Cocoa is a statically laid out UI. GTK has the concept of layout containers and boxes into which you insert controls and tell the box how you want them to be laid out and what to do when the size of the box changes. While it is confusing at first, it very quickly becomes a powerful way to layout a user interface that resizes perfectly when the window or the control size changes. Cocoa has the concept of struts and springs that allow some degree of control over resizing, but it does not give the programmer as much power over window resizing and doesn't handle control resizing at all.
However, the Gtk box model is not perfect, and one of the areas it doesn't work so well is allowing controls to be moved around which complicates animating interfaces which is one area that Cocoa interfaces do very well. So to get around both these problems Apple have added CoreLayout to Lion. Corelayout allows the programmer to add contraints to the interface and describe the relationship between controls and views via these constraints. It is similar to the constraints system used in CoreAnimation but gives the programmer even more control in describing what happens when views change sizes.
Constraints can be created in Interface Builder or in code, however the support in Interface Builder still seems somewhat buggy unfortunately with annoying behaviours and the occasional stack trace dialog. Hopefully future versions will be more stable, the feature seems like it was only added relatively recently to Interface Builder, so save often. We'll first look at how to create constraints in IB, and then move on to creating them by hand in code.
The first thing that needs to be done is to turn on constraints for the nib file. To do this, select the nib file so that the interface builder appears, then click on the file inspector button on the righthand side pane. Then you need to check the option marked "Use Auto Layout".
A warning may come up informing you that you can only run this program on 10.7, but you know that, so just dismiss it.
To demonstrate constraints I just put some simple controls into a window, and drag them until they snap to the blue constraints lines at the edges of the window. These are automatic constraints that are fixed at the correct Cocoa spaces and when the control is snapped to one, it automatically gets constrained. So we expand the second text field to the right until it snaps to the blue constraint line, just the way we did when we created a layout in Cocoa previously, the difference is that now, with CoreLayout turned on, the control is now automatically constrained to always be at that guide line. If we expand the window, the control will move, or expand so that the right hand edge is always the same distance from the window edge.
We have two vertical spaces, one for each text field that dictates how far the top of the text field is away from the top of the window. We have a horizontal space constraining the left edge of the left text field to the left edge of the window, one doing the same for the right edge of the right text field, and we also have one making the space between the two text field constant as well.
If you select a control, you can see the constraints that act upon it, drawn as lines around it:
and if you select a constraint, then the control (or controls) that are affected by the constraint are highlighted in yellow, and the appropriate constraint line is given a drop shadow to stand out:
The first thing that needs to be done is to turn on constraints for the nib file. To do this, select the nib file so that the interface builder appears, then click on the file inspector button on the righthand side pane. Then you need to check the option marked "Use Auto Layout".
A warning may come up informing you that you can only run this program on 10.7, but you know that, so just dismiss it.
To demonstrate constraints I just put some simple controls into a window, and drag them until they snap to the blue constraints lines at the edges of the window. These are automatic constraints that are fixed at the correct Cocoa spaces and when the control is snapped to one, it automatically gets constrained. So we expand the second text field to the right until it snaps to the blue constraint line, just the way we did when we created a layout in Cocoa previously, the difference is that now, with CoreLayout turned on, the control is now automatically constrained to always be at that guide line. If we expand the window, the control will move, or expand so that the right hand edge is always the same distance from the window edge.
In the left hand column, under Objects a new object has been added to the hierarchy: "Constraints". If you expand it then you see all the constraints that are present. Ones with a purple icon are automatic constraints, user-added constraints have a blue icon. The default ones for this interface are
If you select a control, you can see the constraints that act upon it, drawn as lines around it:
and if you select a constraint, then the control (or controls) that are affected by the constraint are highlighted in yellow, and the appropriate constraint line is given a drop shadow to stand out:
There is one final constraint that dictates that the width of the first text field will always be fixed. This means that as the window is expanded in width, the first text field will not expand, but the second will. If you want to test it out, build it and run it, and you can see for yourself. But maybe this default setting is not what you want. Maybe you want both the text fields to expand equally.
Adding Custom Constraints
To do this, you need to add a custom constraint. Select both the text fields, and in Editor->Pin menu select "Widths Equally". If you now look at the constraints list you'll see that the "Width (213)" constraint has been replaced with a "Equal Widths" constraint with a blue icon, and if you select that constraint both text fields turn yellow and the width constraint lines now have an = in a circle on them. Now if you run the program and expand the window, both the fields will expand to fill the space.
But maybe instead of always being equal, you want them to be equal until the left one is 350 pixels wide, and then the right field will keep expanding. So lets add a width constraint to the left hand field. Select it, and in the Editor->Pin menu select "Width". This will add a second user constraint to the field, that says its width must be equal to 213 (or whatever the default width you set). We are able to change the type of the constraint, choosing between equal to, less than or equal to, and more than or equal to. We do this by selecting the constraint and bringing up the Attributes Inspector. If we set the Relation dropdown to "Less than or equal" and set the constant to 350.
(While doing this, you might find that XCode unselects the constraint and reorders the constraints list every time you try to change something. This is quite annoying, but just select the constraint again and continue.)
Now, what happens when you build and run it? If you expand the window, both the text fields will expand until they are 350 pixels wide but then you won't be able to expand the window any more. This is because our two constraints say that the first text field must be less than 350 pixels wide, and that both text fields must be equal width. How can we fix this?
Well, the constraints system has the idea of priority and by default constraints are set to 1,000 which means "Must be fulfilled". If we were to lower one of the priorities to be an optional constraint then it wouldn't need to be fulfilled if it can't be.
So which one should be lowered? Well, the constraint we want to break is the equal width one as we always want the first text field to be less than 350 pixels in width. Set the priority of the "Equal widths" constraint to about 500 and rebuild. Also notice than when a constraint is not "Must be fulfilled" the constraint line becomes broken with dashes.
Now when you build and run, if you expand the window the text fields will remain equal width until the first is 350 and then only the second one will expand.
That's a basic guide to auto layout in Interface Builder. In the next part we'll look at creating constraints in code using the Visual Format Language
Other articles on Corelayout
Part 2:- http://comelearncocoawithme.blogspot.com/2011/08/laying-out-interfaces-automatically_13.html
Other articles on Corelayout
Part 2:- http://comelearncocoawithme.blogspot.com/2011/08/laying-out-interfaces-automatically_13.html
Labels:
AutoLayout,
Cocoa,
CoreLayout,
OSX
Subscribe to:
Posts (Atom)






