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.

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

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:

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:-

No comments:

Post a Comment

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