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



- (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.


Other articles about Corelayout

No comments:

Post a Comment

Note: only a member of this blog may post a comment.