Skip to content

Instantly share code, notes, and snippets.

@kentcb
Created December 2, 2016 02:48
Show Gist options
  • Save kentcb/5c45be9c322f99f831646e9e3388141e to your computer and use it in GitHub Desktop.
Save kentcb/5c45be9c322f99f831646e9e3388141e to your computer and use it in GitHub Desktop.
iOS Auto-layout C# fluent interface
// this code is a heavily modified (and tested) version of https://gist.github.com/praeclarum/6225853
// example usage
this.ContentView.ConstrainLayout(() =>
this.clientNameLabel.Left() == this.ContentView.Left() + Layout.StandardSuperviewSpacing &&
this.clientNameLabel.Top() == this.ContentView.Top() + Layout.StandardSiblingViewSpacing &&
this.createdLabel.Left() == this.clientNameLabel.Right() + Layout.StandardSiblingViewSpacing &&
this.createdLabel.CenterY() == this.ContentView.CenterY() &&
this.createdLabel.Right() == this.ContentView.Right() - Layout.StandardSuperviewSpacing &&
this.referenceLabel.Left() == this.clientNameLabel.Left() &&
this.referenceLabel.Right() == this.createdLabel.Right() &&
this.referenceLabel.Top() == this.clientNameLabel.Bottom() &&
this.referenceLabel.Bottom() == this.ContentView.Bottom() - Layout.StandardSiblingViewSpacing);
namespace FOO
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Foundation;
using ObjCRuntime;
using UIKit;
public static class Layout
{
// the standard spacing between sibling views
public const int StandardSiblingViewSpacing = 8;
// half the standard spacing between sibling views
public const int HalfSiblingViewSpacing = StandardSiblingViewSpacing / 2;
// the standard spacing between a view and its superview
public const int StandardSuperviewSpacing = 20;
// half the standard spacing between superviews
public const int HalfSuperviewSpacing = StandardSuperviewSpacing / 2;
public const float RequiredPriority = (float)UILayoutPriority.Required;
public const float HighPriority = (float)UILayoutPriority.DefaultHigh;
public const float LowPriority = (float)UILayoutPriority.DefaultLow;
#if DEBUG
internal static readonly IDictionary<string, string> constraintSubstitutions = new Dictionary<string, string>();
#endif
public static void ConstrainLayout(this UIView view, Expression<Func<bool>> constraintsExpression, float priority = RequiredPriority)
{
var body = constraintsExpression.Body;
var constraints = FindBinaryExpressionsRecursive(body)
.Select(e =>
{
#if DEBUG
if (ExtractAndRegisterName(e, view))
{
return null;
}
#endif
return CompileConstraint(e, view, priority);
})
.Where(x => x != null)
.ToArray();
view.AddConstraints(constraints);
}
private static IEnumerable<BinaryExpression> FindBinaryExpressionsRecursive(Expression expression)
{
var binaryExpression = expression as BinaryExpression;
if (binaryExpression == null)
{
yield break;
}
if (binaryExpression.NodeType == ExpressionType.AndAlso)
{
foreach (var childBinaryExpression in FindBinaryExpressionsRecursive(binaryExpression.Left))
{
yield return childBinaryExpression;
}
foreach (var childBinaryExpression in FindBinaryExpressionsRecursive(binaryExpression.Right))
{
yield return childBinaryExpression;
}
}
else
{
yield return binaryExpression;
}
}
#if DEBUG
// special case to extract names from the expression, such as this.someControl.Name() == nameof(someControl)
private static bool ExtractAndRegisterName(BinaryExpression binaryExpression, UIView constrainedView)
{
if (binaryExpression.NodeType != ExpressionType.Equal)
{
return false;
}
MethodCallExpression methodCallExpression;
UIView view;
NSLayoutAttribute layoutAttribute;
DetermineConstraintInformationFromExpression(binaryExpression.Left, out methodCallExpression, out view, out layoutAttribute, false);
if (methodCallExpression == null || methodCallExpression.Method.Name != nameof(LayoutExtensions.Name))
{
return false;
}
if (binaryExpression.Right.NodeType != ExpressionType.Constant)
{
throw new NotSupportedException("When assigning a name to a control, only constants are supported.");
}
var name = (string)((ConstantExpression)binaryExpression.Right).Value;
var iOSName = view.Class.Name + ":0x" + view.Handle.ToString("x");
constraintSubstitutions[iOSName] = name;
return true;
}
#endif
private static NSLayoutConstraint CompileConstraint(BinaryExpression binaryExpression, UIView constrainedView, float priority)
{
NSLayoutRelation layoutRelation;
switch (binaryExpression.NodeType)
{
case ExpressionType.Equal:
layoutRelation = NSLayoutRelation.Equal;
break;
case ExpressionType.LessThanOrEqual:
layoutRelation = NSLayoutRelation.LessThanOrEqual;
break;
case ExpressionType.GreaterThanOrEqual:
layoutRelation = NSLayoutRelation.GreaterThanOrEqual;
break;
default:
throw new NotSupportedException("Not a valid relationship for a constraint: " + binaryExpression.NodeType);
}
MethodCallExpression methodCallExpression;
UIView leftView;
NSLayoutAttribute leftLayoutAttribute;
DetermineConstraintInformationFromExpression(binaryExpression.Left, out methodCallExpression, out leftView, out leftLayoutAttribute);
if (leftView != null && leftView != constrainedView)
{
leftView.TranslatesAutoresizingMaskIntoConstraints = false;
}
UIView rightView;
NSLayoutAttribute rightLayoutAttribute;
float multiplier;
float constant;
DetermineConstraintInformationFromExpression(binaryExpression.Right, out rightView, out rightLayoutAttribute, out multiplier, out constant);
if (rightView != null && rightView != constrainedView)
{
rightView.TranslatesAutoresizingMaskIntoConstraints = false;
}
var constraint = NSLayoutConstraint.Create(
leftView,
leftLayoutAttribute,
layoutRelation,
rightView,
rightLayoutAttribute,
multiplier,
constant);
constraint.Priority = priority;
return constraint;
}
private static void DetermineConstraintInformationFromExpression(
Expression expression,
out MethodCallExpression methodCallExpression,
out UIView view,
out NSLayoutAttribute layoutAttribute,
bool throwOnError = true)
{
methodCallExpression = FindExpressionOfType<MethodCallExpression>(expression);
if (methodCallExpression == null)
{
if (throwOnError)
{
throw new NotSupportedException("Constraint expression must be a method call.");
}
else
{
view = null;
layoutAttribute = default(NSLayoutAttribute);
return;
}
}
layoutAttribute = NSLayoutAttribute.NoAttribute;
switch (methodCallExpression.Method.Name)
{
case nameof(LayoutExtensions.Width):
layoutAttribute = NSLayoutAttribute.Width;
break;
case nameof(LayoutExtensions.Height):
layoutAttribute = NSLayoutAttribute.Height;
break;
case nameof(LayoutExtensions.Left):
case nameof(LayoutExtensions.X):
layoutAttribute = NSLayoutAttribute.Left;
break;
case nameof(LayoutExtensions.Top):
case nameof(LayoutExtensions.Y):
layoutAttribute = NSLayoutAttribute.Top;
break;
case nameof(LayoutExtensions.Right):
layoutAttribute = NSLayoutAttribute.Right;
break;
case nameof(LayoutExtensions.Bottom):
layoutAttribute = NSLayoutAttribute.Bottom;
break;
case nameof(LayoutExtensions.CenterX):
layoutAttribute = NSLayoutAttribute.CenterX;
break;
case nameof(LayoutExtensions.CenterY):
layoutAttribute = NSLayoutAttribute.CenterY;
break;
case nameof(LayoutExtensions.Baseline):
layoutAttribute = NSLayoutAttribute.Baseline;
break;
case nameof(LayoutExtensions.Leading):
layoutAttribute = NSLayoutAttribute.Leading;
break;
case nameof(LayoutExtensions.Trailing):
layoutAttribute = NSLayoutAttribute.Trailing;
break;
default:
if (throwOnError)
{
throw new NotSupportedException("Method call '" + methodCallExpression.Method.Name + "' is not recognized as a valid constraint.");
}
break;
}
if (methodCallExpression.Arguments.Count != 1)
{
if (throwOnError)
{
throw new NotSupportedException("Method call '" + methodCallExpression.Method.Name + "' has " + methodCallExpression.Arguments.Count + " arguments, where only 1 is allowed.");
}
else
{
view = null;
return;
}
}
var viewExpression = methodCallExpression.Arguments.FirstOrDefault() as MemberExpression;
if (viewExpression == null)
{
if (throwOnError)
{
throw new NotSupportedException("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)
{
throw new NotSupportedException("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;
}
}
}
private static void DetermineConstraintInformationFromExpression(
Expression expression,
out UIView view,
out NSLayoutAttribute layoutAttribute,
out float multiplier,
out float constant)
{
var viewExpression = expression;
view = null;
layoutAttribute = NSLayoutAttribute.NoAttribute;
multiplier = 1.0f;
constant = 0.0f;
if (viewExpression.NodeType == ExpressionType.Add || viewExpression.NodeType == ExpressionType.Subtract)
{
var binaryExpression = (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)
{
var binaryExpression = (BinaryExpression)viewExpression;
multiplier = Evaluate<float>(binaryExpression.Right);
if (viewExpression.NodeType == ExpressionType.Divide)
{
multiplier = 1 / multiplier;
}
viewExpression = binaryExpression.Left;
}
if (viewExpression is MethodCallExpression)
{
MethodCallExpression methodCallExpression;
DetermineConstraintInformationFromExpression(viewExpression, out methodCallExpression, out view, out layoutAttribute);
}
else
{
// constraint must be something like: view.Width() == 50
constant = Evaluate<float>(viewExpression);
}
}
private static T Evaluate<T>(Expression expression)
{
var result = Evaluate(expression);
if (result is T)
{
return (T)result;
}
return (T)Convert.ChangeType(Evaluate(expression), typeof(T));
}
private static object Evaluate(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant)
{
return ((ConstantExpression)expression).Value;
}
if (expression.NodeType == ExpressionType.MemberAccess)
{
var memberExpression = (MemberExpression)expression;
var member = memberExpression.Member;
if (member.MemberType == MemberTypes.Field)
{
var fieldInfo = (FieldInfo)member;
if (fieldInfo.IsStatic)
{
return fieldInfo.GetValue(null);
}
}
}
return Expression.Lambda(expression).Compile().DynamicInvoke();
}
// searches for an expression of type T within expression, skipping through "irrelevant" nodes
private static T FindExpressionOfType<T>(Expression expression)
where T : Expression
{
while (!(expression is T))
{
switch (expression.NodeType)
{
case ExpressionType.Convert:
expression = ((UnaryExpression)expression).Operand;
break;
default:
return default(T);
}
}
return (T)expression;
}
#if DEBUG
public static class DebugConstraint
{
private delegate IntPtr DescriptionDelegate(IntPtr self, IntPtr sel);
private static DescriptionDelegate replacementDescriptionImplementation = new DescriptionDelegate(Description);
public static void Swizzle()
{
var constraintClass = Class.GetHandle(typeof(NSLayoutConstraint));
var method = class_getInstanceMethod(constraintClass, Selector.GetHandle("description"));
var originalImpl = 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.
var newImpl = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(replacementDescriptionImplementation);
method_setImplementation(method, newImpl);
}
[ObjCRuntime.MonoPInvokeCallback(typeof(DescriptionDelegate))]
public static IntPtr Description(IntPtr self, IntPtr sel)
{
var originalDescriptionPtr = objc_msgSend(self, Selector.GetHandle("customDescription"));
var originalDescription = Runtime.GetNSObject<NSString>(originalDescriptionPtr);
var description = originalDescription.ToString();
foreach (var substitution in Layout.constraintSubstitutions)
{
description = description.Replace(substitution.Key, substitution.Value);
}
return new NSString(description).Handle;
}
[System.Runtime.InteropServices.DllImport("libobjc.dylib")]
static extern IntPtr objc_msgSend(IntPtr handle, IntPtr sel);
[System.Runtime.InteropServices.DllImport("libobjc.dylib")]
static extern IntPtr class_getInstanceMethod(IntPtr c, IntPtr sel);
[System.Runtime.InteropServices.DllImport("libobjc.dylib")]
static extern bool class_addMethod(IntPtr cls, IntPtr name, IntPtr imp, string types);
[System.Runtime.InteropServices.DllImport("libobjc.dylib")]
extern static IntPtr class_getMethodImplementation(IntPtr cls, IntPtr sel);
[System.Runtime.InteropServices.DllImport("libobjc.dylib")]
extern static IntPtr method_setImplementation(IntPtr method, IntPtr imp);
}
#endif
}
}
namespace FOO
{
using UIKit;
// 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 warnings
public static class LayoutExtensions
{
public static int Width(this UIView @this) => 0;
public static int Height(this UIView @this) => 0;
public static int Left(this UIView @this) => 0;
public static int X(this UIView @this) => 0;
public static int Top(this UIView @this) => 0;
public static int Y(this UIView @this) => 0;
public static int Right(this UIView @this) => 0;
public static int Bottom(this UIView @this) => 0;
public static int Baseline(this UIView @this) => 0;
public static int Leading(this UIView @this) => 0;
public static int Trailing(this UIView @this) => 0;
public static int CenterX(this UIView @this) => 0;
public static int CenterY(this UIView @this) => 0;
public static string Name(this UIView @this) => null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment