A straightforward C++ DSL to more concisely define layout constraints.
The standard syntax for defining layout constraints is verbose. For example, to specify the x offset of one button relative to another with a 5 point gap between them, one would write:
self.view addConstraint:[NSLayoutConstraint constraintWithItem:_button2
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:_button1
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:5.0]];
With the layout DSL, this would simply be written as:
View(_button2).left == View(_button1).right + 5.0;
or more naturally as:
View(_button1).right + 5.0 == View(_button2).left;
That's it. When this expression is executed, the NSLayoutConstraint
object will be created and automatically
installed in the nearest common ancestor of _button1
and _button2
.
If you're willing to compile your code as objective-C++, you can access an even more consise constraint definition.
Just #import "UIView+AutoLayoutDSLSugar.h"
and the constraint referred to above can be defined as:
_button1.right + 5.0 == _button2.left;
Now that's just about perfect.
As mentioned before, just declaring a constraint expression is sufficient to install that constraint in the nearest
common ancestor of the referenced views, but if you'd rather install the constraint yourself, you can simply use a
constraint expression in place of an actual NSLayoutConstraint
object in a call to addConstraint:
. The act of
passing the expression to the method will prevent the constraint from being automatically installed.
[self.view addConstraint:View(_button1).left == View(_button2).right + 5.0];
Basically, a constraint expression can be used anywhere an NSLayoutConstraint
object is expected. For example,
multiple constraints can be installed as follows:
[self.view addConstraints:@[View(_button1).right + 5.0 == View(_button2).left,
View(_button2).right + 5.0 == View(_button3).left]];
But why bother when this is so much more concise?
View(_button1).right + 5.0 == View(_button2).left;
View(_button2).right + 5.0 == View(_button3).left;
To specify a constraint on a view relative to its superview, just omit the UIView*
parameter from the View
specification:
View().left + 5.0 == View(_button1).left;
Priorities can be specified in a constraint expression by using the ^
operator, like so:
View(_button1).left == View(_button2).right + 5.0 ^ 999.0;
You can add an identifier to a single constraint or a group of constraints to more easily remove and replace them, when necessary. There are a few ways to accomplish this.
Just like adding priorities, identifiers can be added to a constraint expression by using the ^
operator, like so:
View(_image).width == View(_image).height * aspectRatio ^ @"maintainAspect";
You can group constraints by assigning them the same identifier:
View(_avatar).left == View().left + 5.0 ^ @"A group of constraints";
View(_label).left == View(_avatar).right + StandardHorizontalGap ^ @"A group of constraints";
But a much cleaner way is by using the contraint grouping macros:
BeginConstraintGroup(@"A group of constraints")
View(_avatar).left == View().left + 5.0;
View(_label).left == View(_avatar).right + StandardHorizontalGap;
EndConstraintGroup;
To find a single constraint given its identifier:
NSLayoutConstraint *constraint = [self.view constraintWithID:@"identifier"];
To find all constraints with a given identifier:
NSArray *constraint = [self.view constraintsWithID:@"identifier"];
To find all constraints referencing a given view:
NSArray *constraints = [self.view constraintsReferencingView:_button1];
To remove a constraint, simply call remove
NSLayoutConstraint *constraint = [self.view constraintWithID:@"identifier"];
[constraint remove];
This DSL requires that your source file be compiled as Objective-C++ (just change its extension to .mm). This may be a concern to some. If you don't want to deal with any potential problems this may cause, you can still use the DSL by defining all of your constraints in a separate file using categories.
I used some unorthodox methods to acheive the results I was looking for. The ability to have a constraint
auto-install itself is acheived by installing the newly built NSLayoutContraint
object from the destructor of the
internal ConstraintBuilder
object. The ability to use a constraint expression in place of an NSLayoutConstraint
object is acheived through an overloaded cast operator on the internal ConstraintBuilder
class. In order to prevent
multiple constraint installs, any casting the ConstraintBuilder
object will transfer ownership of the newly built
NSLayoutConstraint
object to the caller, so when the ConstraintBuilder
object is destroyed, there is no
NSLayoutConstraint
object to install. Unfortunately, this prevents directly obtaining a pointer to an
auto-installed constraint:
// The following constraint has not been automatically installed
NSLayoutConstraint *constraint = View(_avatar).left == View().left + 5.0;
This is a small price to pay, since the auto-install behavior can easily be invoked:
[constraint install];
Regardless, this kind of magic behavior will be disliked by some.
Unfortunately, the compiler/static analyzer will most likely try to warn you that your free-standing constraint expressions are unused.
The only way to prevent this is to disable the warning. You can wrap your constraint expressions like so:
_Pragma( "clang diagnostic push" )
_Pragma( "clang diagnostic ignored \"-Wunused-value\" " )
View(_avatar).left == View().left + 5.0;
_Pragma( "clang diagnostic pop")
But that's pretty ugly. To clean this up a bit, I've added a couple of macros to wrap constraints with:
BeginConstraints
View(_avatar).left == View().left + 5.0;
EndConstraints;
The constraint grouping macros BeginConstraintGroup()
and EndConstraintGroup
also disable these warnings.
The main expression syntax is pretty fixed, but the method of adding identifiers and priorities may change.
The code currently allows for these to be added with the ^
operator, and also the ,
operator. I'm open
to suggested improvements.
There are quite a few attempts to improve on the default constraint syntax. These are just a few that caught my attention:
AutoLayoutDSL is available with CocoaPods[http://cocoapods.org], to install simply add the following line to your Podfile:
pod AutoLayoutDSL
David Whetstone david@humblehacker.com
The constraint expressions are my own design, but the ideas of constraint identification, grouping, and self-install are all thanks to Erica Sadun and her excellent book iOS Auto-Layout Demystified. Much of the code related to these features are massaged versions of the sample code included with that book, found here.
AutoLayoutDSL is available under the MIT license. See the LICENSE file for more info.