Reducing Auto Layout Friction

02 Jun 2015

In the 18 months or so I’ve been doing iOS development, my biggest struggles have been with layout. Over the years, iOS layout infrastructure has evolved drastically: from manually positioning views, to using springs, struts and auto-resizing masks. And with iOS 6 onwards, auto layout.

From early on I decided to utilise auto layout because it ostensibly offers more power whilst reducing the amount of work required to support multiple screen sizes and orientations (with some cost to performance, of course). And it would save me having to learn about all the “legacy” layout stuff.

But even auto layout is far from a walk in the park. Here’s an example of some C# code that creates an NSLayoutConstraint using the UIKit APIs:

In this case, we’re ensuring the left side of our checkmarkImage is positioned 20px in from the left side of ContentView. Just to be clear: that’s one constraint. A non-trivial view is likely to have a dozen or more. I’m sure you can imagine the write-only code that would result.

So having decided to use auto layout I realised I needed to find a better way to leverage it into my code base. It wasn’t long before I found Frank Krueger’s (@praeclarum) Easy Layout gist. The idea of Frank’s code was to allow constraints to be specified via expressions. Our example above could instead be written as:

This was an excellent starting point for facilitating auto layout within my iOS applications without requiring a wall of code. But over the months I’ve tweaked Frank’s code to better suit my needs. I want to document and discuss most of those changes here.

I’ve just noticed Frank has updated his gist several times too, and some of the things he’s added mirror my changes. I’ll point out where that’s the case below.

Constants

OK, a small thing first. I wanted to remove the need for hard-coded sizes in constraint expressions. Most of the time we care about “standard” spacing, such as that between a view and its superview, and between sibling views. To that end, I added several constants to the Layout class:

The second group of constants allows us to more easily specify priorities when calling SetContentHuggingPriority and SetContentCompressionResistancePriority. Using UILayoutPriority directly means we need to cast:

Avoiding Compiler Warnings

In Xamarin Studio, comparing two float values results in compiler warnings. Because I use Xamarin Studio for some projects, this was incredibly annoying. Basically, I would see warnings like this everywhere:

To solve this, I created the LayoutExtensions class mentioned above. By using extension methods rather than the existing properties (such as Frame.Left) I could both reduce the verbosity of constraint code, and get around the compiler warnings. The extension methods all return int:

publicstaticintLeft(thisUIView@this)=>0;

Ultimately, the type doesn’t matter because the method invocation is just a marker picked up by the expression parsing logic inside the Layout class. By returning int we’re ensuring that all constraints are comparing one int to another, thus avoiding the compiler warnings.

Priorities

Frank has since added support for this.

Sometimes we want our constraints to act at a lower priority, perhaps to avoid ambiguity between our constraints and system-provided constraints. To facilitate this, I added an optional priority parameter (of type float) to ConstrainLayout, which defaults to Layout.RequiredPriority.

Naming Controls for Improved Diagnostics

From day 1, one thing I absolutely hated about auto layout were the error messages one sees when constraints cannot be satisfied. Here’s an example:

Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7fd04600 V:|-(8)-[UILabel:0x7fd22640' '] (Names:'|':UITableViewCellContentView:0x7fd304d0 )>",
"<NSLayoutConstraint:0x7fd03f00 V:|-(0)-[UILabel:0x7fd22640' '] (Names:'|':UITableViewCellContentView:0x7fd304d0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fd04600 V:|-(8)-[UILabel:0x7fd22640' '] (Names:'|':UITableViewCellContentView:0x7fd304d0 )>

Riiiiight…

I can’t tell you how many times I wanted to throw my Mac in the pool as a result of these messages. Yes, we can spit out the Handle property of our various UIView somewhere and then manually match up controls those identifiers with those in the error message, but by all the gods that is painful.

For a long while I just put up with it - I didn’t feel as though I had any recourse, considering there is no way to set view identifiers in iOS. But recently I was pushed over the edge by a constraint ambiguity message that I just could not fathom. To that end, I set out to solve the problem of the opaque error messages.

It turned out to be really tricky. If it wasn’t for the help of Xamarin’s Rolf Kvinge (@rolfkvinge), I don’t think I ever would have cracked this nut. The full details of my failed attempts are documented in bugzilla, so I won’t bore you with the details here.

The eventual solution (again, thanks to Rolf) involves “swizzling”, which is the dubious practice of replacing an existing selector at runtime. Because of this, and because this feature is solely for diagnostic purposes, almost all code related to naming controls is only included in DEBUG builds.

The upshot is that we can specify names for our controls like this (this will still build for non-DEBUG builds, but the calls to Name will have no effect):

Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7fed5410 V:|-(8)-[clientNameLabel' '] (Names: '|':ContentView )>",
"<NSLayoutConstraint:0x7fed5480 V:|-(0)-[clientNameLabel' '] (Names: '|':ContentView )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fed5410 V:|-(8)-[clientNameLabel' '] (Names: '|':ContentView )>

Much better!

The Code

Firstly, if you want to enable the support for naming controls, be sure to include this first thing in your AppDelegate:

#if DEBUG
Layout.DebugConstraint.Swizzle();#endif

And here is all the code, including unit tests:

Layout.cs

// this code is a heavily modified (and tested) version of https://gist.github.com/praeclarum/6225853namespaceiOS.Utility{usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Linq.Expressions;usingSystem.Reflection;usingFoundation;usingObjCRuntime;usingUIKit;publicstaticclassLayout{// the standard spacing between sibling viewspublicconstintStandardSiblingViewSpacing=8;// half the standard spacing between sibling viewspublicconstintHalfSiblingViewSpacing=StandardSiblingViewSpacing/2;// the standard spacing between a view and its superviewpublicconstintStandardSuperviewSpacing=20;// half the standard spacing between superviewspublicconstintHalfSuperviewSpacing=StandardSuperviewSpacing/2;publicconstfloatRequiredPriority=(float)UILayoutPriority.Required;publicconstfloatHighPriority=(float)UILayoutPriority.DefaultHigh;publicconstfloatLowPriority=(float)UILayoutPriority.DefaultLow;#if DEBUG
internalstaticreadonlyIDictionary<string,string>constraintSubstitutions=newDictionary<string,string>();#endif
publicstaticvoidConstrainLayout(thisUIViewview,Expression<Func<bool>>constraintsExpression,floatpriority=RequiredPriority){varbody=constraintsExpression.Body;varconstraints=FindBinaryExpressionsRecursive(body).Select(e=>{#if DEBUG
if(ExtractAndRegisterName(e,view)){returnnull;}#endif
returnCompileConstraint(e,view,priority);}).Where(x=>x!=null).ToArray();view.AddConstraints(constraints);}privatestaticIEnumerable<BinaryExpression>FindBinaryExpressionsRecursive(Expressionexpression){varbinaryExpression=expressionasBinaryExpression;if(binaryExpression==null){yieldbreak;}if(binaryExpression.NodeType==ExpressionType.AndAlso){foreach(varchildBinaryExpressioninFindBinaryExpressionsRecursive(binaryExpression.Left)){yieldreturnchildBinaryExpression;}foreach(varchildBinaryExpressioninFindBinaryExpressionsRecursive(binaryExpression.Right)){yieldreturnchildBinaryExpression;}}else{yieldreturnbinaryExpression;}}#if DEBUG
// special case to extract names from the expression, such as this.someControl.Name() == nameof(someControl)privatestaticboolExtractAndRegisterName(BinaryExpressionbinaryExpression,UIViewconstrainedView){if(binaryExpression.NodeType!=ExpressionType.Equal){returnfalse;}MethodCallExpressionmethodCallExpression;UIViewview;NSLayoutAttributelayoutAttribute;DetermineConstraintInformationFromExpression(binaryExpression.Left,outmethodCallExpression,outview,outlayoutAttribute,false);if(methodCallExpression==null||methodCallExpression.Method.Name!=nameof(LayoutExtensions.Name)){returnfalse;}if(binaryExpression.Right.NodeType!=ExpressionType.Constant){thrownewNotSupportedException("When assigning a name to a control, only constants are supported.");}varname=(string)((ConstantExpression)binaryExpression.Right).Value;variOSName=view.Class.Name+":0x"+view.Handle.ToString("x");constraintSubstitutions[iOSName]=name;returntrue;}#endif
privatestaticNSLayoutConstraintCompileConstraint(BinaryExpressionbinaryExpression,UIViewconstrainedView,floatpriority){NSLayoutRelationlayoutRelation;switch(binaryExpression.NodeType){caseExpressionType.Equal:layoutRelation=NSLayoutRelation.Equal;break;caseExpressionType.LessThanOrEqual:layoutRelation=NSLayoutRelation.LessThanOrEqual;break;caseExpressionType.GreaterThanOrEqual:layoutRelation=NSLayoutRelation.GreaterThanOrEqual;break;default:thrownewNotSupportedException("Not a valid relationship for a constraint: "+binaryExpression.NodeType);}MethodCallExpressionmethodCallExpression;UIViewleftView;NSLayoutAttributeleftLayoutAttribute;DetermineConstraintInformationFromExpression(binaryExpression.Left,outmethodCallExpression,outleftView,outleftLayoutAttribute);if(leftView!=null&&leftView!=constrainedView){leftView.TranslatesAutoresizingMaskIntoConstraints=false;}UIViewrightView;NSLayoutAttributerightLayoutAttribute;floatmultiplier;floatconstant;DetermineConstraintInformationFromExpression(binaryExpression.Right,outrightView,outrightLayoutAttribute,outmultiplier,outconstant);if(rightView!=null&&rightView!=constrainedView){rightView.TranslatesAutoresizingMaskIntoConstraints=false;}varconstraint=NSLayoutConstraint.Create(leftView,leftLayoutAttribute,layoutRelation,rightView,rightLayoutAttribute,multiplier,constant);constraint.Priority=priority;returnconstraint;}privatestaticvoidDetermineConstraintInformationFromExpression(Expressionexpression,outMethodCallExpressionmethodCallExpression,outUIViewview,outNSLayoutAttributelayoutAttribute,boolthrowOnError=true){methodCallExpression=FindExpressionOfType<MethodCallExpression>(expression);if(methodCallExpression==null){if(throwOnError){thrownewNotSupportedException("Constraint expression must be a method call.");}else{view=null;layoutAttribute=default(NSLayoutAttribute);return;}}layoutAttribute=NSLayoutAttribute.NoAttribute;switch(methodCallExpression.Method.Name){casenameof(LayoutExtensions.Width):layoutAttribute=NSLayoutAttribute.Width;break;casenameof(LayoutExtensions.Height):layoutAttribute=NSLayoutAttribute.Height;break;casenameof(LayoutExtensions.Left):casenameof(LayoutExtensions.X):layoutAttribute=NSLayoutAttribute.Left;break;casenameof(LayoutExtensions.Top):casenameof(LayoutExtensions.Y):layoutAttribute=NSLayoutAttribute.Top;break;casenameof(LayoutExtensions.Right):layoutAttribute=NSLayoutAttribute.Right;break;casenameof(LayoutExtensions.Bottom):layoutAttribute=NSLayoutAttribute.Bottom;break;casenameof(LayoutExtensions.CenterX):layoutAttribute=NSLayoutAttribute.CenterX;break;casenameof(LayoutExtensions.CenterY):layoutAttribute=NSLayoutAttribute.CenterY;break;casenameof(LayoutExtensions.Baseline):layoutAttribute=NSLayoutAttribute.Baseline;break;casenameof(LayoutExtensions.Leading):layoutAttribute=NSLayoutAttribute.Leading;break;casenameof(LayoutExtensions.Trailing):layoutAttribute=NSLayoutAttribute.Trailing;break;default:if(throwOnError){thrownewNotSupportedException("Method call '"+methodCallExpression.Method.Name+"' is not recognized as a valid constraint.");}break;}if(methodCallExpression.Arguments.Count!=1){if(throwOnError){thrownewNotSupportedException("Method call '"+methodCallExpression.Method.Name+"' has "+methodCallExpression.Arguments.Count+" arguments, where only 1 is allowed.");}else{view=null;return;}}varviewExpression=methodCallExpression.Arguments.FirstOrDefault()asMemberExpression;if(viewExpression==null){if(throwOnError){thrownewNotSupportedException("The argument to method call '"+methodCallExpression.Method.Name+"' must be a member expression that resolves to the view being constrained.");}else{view=null;return;}}view=Evaluate<UIView>(viewExpression);if(view==null){if(throwOnError){thrownewNotSupportedException("The argument to method call '"+methodCallExpression.Method.Name+"' resolved to null, so the view to be constrained could not be determined.");}else{view=null;return;}}}privatestaticvoidDetermineConstraintInformationFromExpression(Expressionexpression,outUIViewview,outNSLayoutAttributelayoutAttribute,outfloatmultiplier,outfloatconstant){varviewExpression=expression;view=null;layoutAttribute=NSLayoutAttribute.NoAttribute;multiplier=1.0f;constant=0.0f;if(viewExpression.NodeType==ExpressionType.Add||viewExpression.NodeType==ExpressionType.Subtract){varbinaryExpression=(BinaryExpression)viewExpression;constant=Evaluate<float>(binaryExpression.Right);if(viewExpression.NodeType==ExpressionType.Subtract){constant=-constant;}viewExpression=binaryExpression.Left;}if(viewExpression.NodeType==ExpressionType.Multiply||viewExpression.NodeType==ExpressionType.Divide){varbinaryExpression=(BinaryExpression)viewExpression;multiplier=Evaluate<float>(binaryExpression.Right);if(viewExpression.NodeType==ExpressionType.Divide){multiplier=1/multiplier;}viewExpression=binaryExpression.Left;}if(viewExpressionisMethodCallExpression){MethodCallExpressionmethodCallExpression;DetermineConstraintInformationFromExpression(viewExpression,outmethodCallExpression,outview,outlayoutAttribute);}else{// constraint must be something like: view.Width() == 50constant=Evaluate<float>(viewExpression);}}privatestaticTEvaluate<T>(Expressionexpression){varresult=Evaluate(expression);if(resultisT){return(T)result;}return(T)Convert.ChangeType(Evaluate(expression),typeof(T));}privatestaticobjectEvaluate(Expressionexpression){if(expression.NodeType==ExpressionType.Constant){return((ConstantExpression)expression).Value;}if(expression.NodeType==ExpressionType.MemberAccess){varmemberExpression=(MemberExpression)expression;varmember=memberExpression.Member;if(member.MemberType==MemberTypes.Field){varfieldInfo=(FieldInfo)member;if(fieldInfo.IsStatic){returnfieldInfo.GetValue(null);}}}returnExpression.Lambda(expression).Compile().DynamicInvoke();}// searches for an expression of type T within expression, skipping through "irrelevant" nodesprivatestaticTFindExpressionOfType<T>(Expressionexpression)whereT:Expression{while(!(expressionisT)){switch(expression.NodeType){caseExpressionType.Convert:expression=((UnaryExpression)expression).Operand;break;default:returndefault(T);}}return(T)expression;}#if DEBUG
publicstaticclassDebugConstraint{privatedelegateIntPtrDescriptionDelegate(IntPtrself,IntPtrsel);privatestaticDescriptionDelegatereplacementDescriptionImplementation=newDescriptionDelegate(Description);publicstaticvoidSwizzle(){varconstraintClass=Class.GetHandle(typeof(NSLayoutConstraint));varmethod=class_getInstanceMethod(constraintClass,Selector.GetHandle("description"));varoriginalImpl=class_getMethodImplementation(constraintClass,Selector.GetHandle("description"));// add the original implementation to respond to 'customDescription'class_addMethod(constraintClass,Selector.GetHandle("customDescription"),originalImpl,"@@:");// replace the original implementation with our own for the 'descriptor' method.varnewImpl=System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(replacementDescriptionImplementation);method_setImplementation(method,newImpl);}[ObjCRuntime.MonoPInvokeCallback(typeof(DescriptionDelegate))]publicstaticIntPtrDescription(IntPtrself,IntPtrsel){varoriginalDescriptionPtr=objc_msgSend(self,Selector.GetHandle("customDescription"));varoriginalDescription=Runtime.GetNSObject<NSString>(originalDescriptionPtr);vardescription=originalDescription.ToString();foreach(varsubstitutioninLayout.constraintSubstitutions){description=description.Replace(substitution.Key,substitution.Value);}returnnewNSString(description).Handle;}[System.Runtime.InteropServices.DllImport("libobjc.dylib")]staticexternIntPtrobjc_msgSend(IntPtrhandle,IntPtrsel);[System.Runtime.InteropServices.DllImport("libobjc.dylib")]staticexternIntPtrclass_getInstanceMethod(IntPtrc,IntPtrsel);[System.Runtime.InteropServices.DllImport("libobjc.dylib")]staticexternboolclass_addMethod(IntPtrcls,IntPtrname,IntPtrimp,stringtypes);[System.Runtime.InteropServices.DllImport("libobjc.dylib")]externstaticIntPtrclass_getMethodImplementation(IntPtrcls,IntPtrsel);[System.Runtime.InteropServices.DllImport("libobjc.dylib")]externstaticIntPtrmethod_setImplementation(IntPtrmethod,IntPtrimp);}#endif
}}

LayoutExtensions.cs

namespaceiOS.Utility{usingUIKit;// provides extensions that should be used when laying out via the Layout class// note the use of ints here rather than floats because comparing floats in our constraint expressions results in annoying compiler warningspublicstaticclassLayoutExtensions{publicstaticintWidth(thisUIView@this)=>0;publicstaticintHeight(thisUIView@this)=>0;publicstaticintLeft(thisUIView@this)=>0;publicstaticintX(thisUIView@this)=>0;publicstaticintTop(thisUIView@this)=>0;publicstaticintY(thisUIView@this)=>0;publicstaticintRight(thisUIView@this)=>0;publicstaticintBottom(thisUIView@this)=>0;publicstaticintBaseline(thisUIView@this)=>0;publicstaticintLeading(thisUIView@this)=>0;publicstaticintTrailing(thisUIView@this)=>0;publicstaticintCenterX(thisUIView@this)=>0;publicstaticintCenterY(thisUIView@this)=>0;publicstaticstringName(thisUIView@this)=>null;}}

LayoutFixture.cs

namespaceUnitTests.iOS.Utility{usingSystem;usingUIKit;usingiOS.Utility;usingXunit;publicsealedclassLayoutFixture{[Fact]publicvoidconstrain_layout_throws_if_relationship_type_is_invalid(){varview=newUIView();varex=Assert.Throws<NotSupportedException>(()=>view.ConstrainLayout(()=>view.Left()>view.Right()));Assert.Equal("Not a valid relationship for a constraint: GreaterThan",ex.Message);}[Fact]publicvoidconstrain_layout_throws_if_constraint_is_not_a_method_call(){varview=newUIView();varex=Assert.Throws<NotSupportedException>(()=>view.ConstrainLayout(()=>view.ExclusiveTouch==true));Assert.Equal("Constraint expression must be a method call.",ex.Message);}[Fact]publicvoidconstrain_layout_throws_if_method_is_not_recognized(){varview=newUIView();varex=Assert.Throws<NotSupportedException>(()=>view.ConstrainLayout(()=>view.GetType()==null));Assert.Equal("Method call 'GetType' is not recognized as a valid constraint.",ex.Message);}[Fact]publicvoidconstrain_layout_throws_if_the_method_call_has_the_wrong_number_of_arguments(){varview=newUIView();varviewImposter=newViewImposter();varex=Assert.Throws<NotSupportedException>(()=>view.ConstrainLayout(()=>viewImposter.Right(0,0)==viewImposter.Right(0,0)));Assert.Equal("Method call 'Right' has 2 arguments, where only 1 is allowed.",ex.Message);}[Fact]publicvoidconstrain_layout_throws_if_the_argument_to_the_method_is_not_a_member_expression(){varview=newUIView();varviewImposter=newViewImposter();varex=Assert.Throws<NotSupportedException>(()=>view.ConstrainLayout(()=>viewImposter.Left(0)==viewImposter.Left(0)));Assert.Equal("The argument to method call 'Left' must be a member expression that resolves to the view being constrained.",ex.Message);}[Fact]publicvoidconstrain_layout_throws_if_view_is_null(){UIViewview=null;varex=Assert.Throws<NotSupportedException>(()=>view.ConstrainLayout(()=>view.Left()==view.Right()));Assert.Equal("The argument to method call 'Left' resolved to null, so the view to be constrained could not be determined.",ex.Message);}[Fact]publicvoidconstrain_layout_allows_constraints_with_no_multiplier_or_constant_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left());varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(1f,constraints[0].Multiplier);Assert.Equal(0f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_a_multiplier_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Width()==superView.Width()*2);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Width,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Width,constraints[0].SecondAttribute);Assert.Equal(2f,constraints[0].Multiplier);Assert.Equal(0f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_a_multiplier_via_division_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Width()==superView.Width()/2);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Width,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Width,constraints[0].SecondAttribute);Assert.Equal(0.5f,constraints[0].Multiplier);Assert.Equal(0f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_a_constant_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left()+20);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(1f,constraints[0].Multiplier);Assert.Equal(20f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_a_negative_constant_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left()-20);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(1f,constraints[0].Multiplier);Assert.Equal(-20f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_a_dynamically_evaluated_constant_to_be_configured(){varsomeNumber=50;varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left()+(someNumber*2));varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(1f,constraints[0].Multiplier);Assert.Equal(100f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_both_a_multiplier_and_constant_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left()*2+100);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(2f,constraints[0].Multiplier);Assert.Equal(100f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_both_a_multiplier_and_negative_constant_to_be_configured(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left()*2-100);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(2f,constraints[0].Multiplier);Assert.Equal(-100f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_constraints_with_both_a_dynamic_multiplier_and_dynamic_constant_to_be_configured(){varsomeNumber=5;varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Left()==superView.Left()*(2+someNumber)+(someNumber*10));varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].FirstAttribute);Assert.Same(superView,constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.Left,constraints[0].SecondAttribute);Assert.Equal(7f,constraints[0].Multiplier);Assert.Equal(50f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_allows_a_constraint_against_a_constant_only(){varsuperView=newUIView();varview=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubview(view);superView.ConstrainLayout(()=>view.Width()==50);varconstraints=superView.Constraints;Assert.Equal(1,constraints.Length);Assert.Same(view,constraints[0].FirstItem);Assert.Equal(NSLayoutAttribute.Width,constraints[0].FirstAttribute);Assert.Null(constraints[0].SecondItem);Assert.Equal(NSLayoutAttribute.NoAttribute,constraints[0].SecondAttribute);Assert.Equal(1f,constraints[0].Multiplier);Assert.Equal(50f,constraints[0].Constant);});}[Fact]publicvoidconstrain_layout_sets_translates_autoresizing_mask_into_constraints_to_false_for_any_subviews_of_the_constrained_view(){varsuperView=newUIView();varsubView1=newUIView();varsubView2=newUIView();superView.InvokeOnMainThread(()=>{superView.AddSubviews(subView1,subView2);superView.ConstrainLayout(()=>subView1.Left()==superView.Left()&&subView2.Left()==superView.Left());Assert.True(superView.TranslatesAutoresizingMaskIntoConstraints);Assert.False(subView1.TranslatesAutoresizingMaskIntoConstraints);Assert.False(subView2.TranslatesAutoresizingMaskIntoConstraints);});}#regionSupportingTypesprivateclassViewImposter{publicintLeft(intsomeArg){return0;}publicintRight(intfirst,intsecond){return0;}}#endregion}}