Skip to content

Instantly share code, notes, and snippets.

@praeclarum
Last active April 21, 2023 10:28
Show Gist options
  • Save praeclarum/6225853 to your computer and use it in GitHub Desktop.
Save praeclarum/6225853 to your computer and use it in GitHub Desktop.
**OUT OF DATE** Please use the NuGet package https://www.nuget.org/packages/EasyLayout or the code on GitHub https://github.com/praeclarum/EasyLayout. (EasyLayout makes writing auto layout code in Xamarin.iOS easier. See [the example](https://gist.github.com/praeclarum/8185036) for hints on how to use this library.)
//
// Copyright (c) 2013-2015 Frank A. Krueger
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using UIKit;
namespace Praeclarum.UI
{
public static class Layout
{
/// <summary>
/// <para>Constrains the layout of subviews according to equations and
/// inequalities specified in <paramref name="constraints"/>. Issue
/// multiple constraints per call using the &amp;&amp; operator.</para>
/// <para>e.g. button.Frame.Left &gt;= text.Frame.Right + 22 &amp;&amp;
/// button.Frame.Width == View.Frame.Width * 0.42f</para>
/// </summary>
/// <param name="view">The superview laying out the referenced subviews.</param>
/// <param name="constraints">Constraint equations and inequalities.</param>
public static NSLayoutConstraint[] ConstrainLayout (this UIView view, Expression<Func<bool>> constraints)
{
return ConstrainLayout (view, constraints, UILayoutPriority.Required);
}
/// <summary>
/// <para>Constrains the layout of subviews according to equations and
/// inequalities specified in <paramref name="constraints"/>. Issue
/// multiple constraints per call using the &amp;&amp; operator.</para>
/// <para>e.g. button.Frame.Left &gt;= text.Frame.Right + 22 &amp;&amp;
/// button.Frame.Width == View.Frame.Width * 0.42f</para>
/// </summary>
/// <param name="view">The superview laying out the referenced subviews.</param>
/// <param name="constraints">Constraint equations and inequalities.</param>
/// <param name = "priority">The priority of the constraints</param>
public static NSLayoutConstraint[] ConstrainLayout (this UIView view, Expression<Func<bool>> constraints, UILayoutPriority priority)
{
var body = constraints.Body;
var exprs = new List<BinaryExpression> ();
FindConstraints (body, exprs);
var layoutConstraints = exprs.Select (e => CompileConstraint (e, view)).ToArray ();
if (layoutConstraints.Length > 0) {
foreach (var c in layoutConstraints) {
c.Priority = (float)priority;
}
view.AddConstraints (layoutConstraints);
}
return layoutConstraints;
}
static NSLayoutConstraint CompileConstraint (BinaryExpression expr, UIView constrainedView)
{
var rel = NSLayoutRelation.Equal;
switch (expr.NodeType) {
case ExpressionType.Equal:
rel = NSLayoutRelation.Equal;
break;
case ExpressionType.LessThanOrEqual:
rel = NSLayoutRelation.LessThanOrEqual;
break;
case ExpressionType.GreaterThanOrEqual:
rel = NSLayoutRelation.GreaterThanOrEqual;
break;
default:
throw new NotSupportedException ("Not a valid relationship for a constrain.");
}
var left = GetViewAndAttribute (expr.Left);
if (left.Item1 != constrainedView) {
left.Item1.TranslatesAutoresizingMaskIntoConstraints = false;
}
var right = GetRight (expr.Right);
return NSLayoutConstraint.Create (
left.Item1, left.Item2,
rel,
right.Item1, right.Item2,
right.Item3, right.Item4);
}
static Tuple<UIView, NSLayoutAttribute, float, float> GetRight (Expression expr)
{
var r = expr;
UIView view = null;
NSLayoutAttribute attr = NSLayoutAttribute.NoAttribute;
var mul = 1.0f;
var add = 0.0f;
var pos = true;
if (r.NodeType == ExpressionType.Add || r.NodeType == ExpressionType.Subtract) {
var rb = (BinaryExpression)r;
if (IsConstant (rb.Left)) {
add = ConstantValue (rb.Left);
if (r.NodeType == ExpressionType.Subtract) {
pos = false;
}
r = rb.Right;
}
else if (IsConstant (rb.Right)) {
add = ConstantValue (rb.Right);
if (r.NodeType == ExpressionType.Subtract) {
add = -add;
}
r = rb.Left;
}
else {
throw new NotSupportedException ("Addition only supports constants: " + rb.Right.NodeType);
}
}
if (r.NodeType == ExpressionType.Multiply) {
var rb = (BinaryExpression)r;
if (IsConstant (rb.Left)) {
mul = ConstantValue (rb.Left);
r = rb.Right;
}
else if (IsConstant (rb.Right)) {
mul = ConstantValue (rb.Right);
r = rb.Left;
}
else {
throw new NotSupportedException ("Multiplication only supports constants.");
}
}
if (IsConstant (r)) {
add = Convert.ToSingle (Eval (r));
} else if (r.NodeType == ExpressionType.MemberAccess || r.NodeType == ExpressionType.Call) {
var t = GetViewAndAttribute (r);
view = t.Item1;
attr = t.Item2;
} else {
throw new NotSupportedException ("Unsupported layout expression node type " + r.NodeType);
}
if (!pos)
mul = -mul;
return Tuple.Create (view, attr, mul, add);
}
static bool IsConstant (Expression expr)
{
if (expr.NodeType == ExpressionType.Constant)
return true;
if (expr.NodeType == ExpressionType.MemberAccess) {
var mexpr = (MemberExpression)expr;
var m = mexpr.Member;
if (m.MemberType == MemberTypes.Field) {
return true;
}
return false;
}
if (expr.NodeType == ExpressionType.Convert) {
var cexpr = (UnaryExpression)expr;
return IsConstant (cexpr.Operand);
}
return false;
}
static float ConstantValue (Expression expr)
{
return Convert.ToSingle (Eval (expr));
}
static Tuple<UIView, NSLayoutAttribute> GetViewAndAttribute (Expression expr)
{
var attr = NSLayoutAttribute.NoAttribute;
MemberExpression frameExpr = null;
var fExpr = expr as MethodCallExpression;
if (fExpr != null) {
switch (fExpr.Method.Name) {
case "GetMidX":
case "GetCenterX":
attr = NSLayoutAttribute.CenterX;
break;
case "GetMidY":
case "GetCenterY":
attr = NSLayoutAttribute.CenterY;
break;
case "GetBaseline":
attr = NSLayoutAttribute.Baseline;
break;
default:
throw new NotSupportedException ("Method " + fExpr.Method.Name + " is not recognized.");
}
frameExpr = fExpr.Arguments.FirstOrDefault () as MemberExpression;
}
if (attr == NSLayoutAttribute.NoAttribute) {
var memExpr = expr as MemberExpression;
if (memExpr == null)
throw new NotSupportedException ("Left hand side of a relation must be a member expression, instead it is " + expr);
switch (memExpr.Member.Name) {
case "Width":
attr = NSLayoutAttribute.Width;
break;
case "Height":
attr = NSLayoutAttribute.Height;
break;
case "Left":
case "X":
attr = NSLayoutAttribute.Left;
break;
case "Top":
case "Y":
attr = NSLayoutAttribute.Top;
break;
case "Right":
attr = NSLayoutAttribute.Right;
break;
case "Bottom":
attr = NSLayoutAttribute.Bottom;
break;
default:
throw new NotSupportedException ("Property " + memExpr.Member.Name + " is not recognized.");
}
frameExpr = memExpr.Expression as MemberExpression;
}
if (frameExpr == null)
throw new NotSupportedException ("Constraints should use the Frame or Bounds property of views.");
var viewExpr = frameExpr.Expression;
var view = Eval (viewExpr) as UIView;
if (view == null)
throw new NotSupportedException ("Constraints only apply to views.");
return Tuple.Create (view, attr);
}
static object Eval (Expression expr)
{
if (expr.NodeType == ExpressionType.Constant) {
return ((ConstantExpression)expr).Value;
}
if (expr.NodeType == ExpressionType.MemberAccess) {
var mexpr = (MemberExpression)expr;
var m = mexpr.Member;
if (m.MemberType == MemberTypes.Field) {
var f = (FieldInfo)m;
var v = f.GetValue (Eval (mexpr.Expression));
return v;
}
}
if (expr.NodeType == ExpressionType.Convert) {
var cexpr = (UnaryExpression)expr;
var op = Eval (cexpr.Operand);
if (cexpr.Method != null) {
return cexpr.Method.Invoke (null, new[]{ op });
} else {
return Convert.ChangeType (op, cexpr.Type);
}
}
return Expression.Lambda (expr).Compile ().DynamicInvoke ();
}
static void FindConstraints (Expression expr, List<BinaryExpression> constraintExprs)
{
var b = expr as BinaryExpression;
if (b == null)
return;
if (b.NodeType == ExpressionType.AndAlso) {
FindConstraints (b.Left, constraintExprs);
FindConstraints (b.Right, constraintExprs);
} else {
constraintExprs.Add (b);
}
}
/// <summary>
/// The baseline of the view whose frame is viewFrame. Use only when defining constraints.
/// </summary>
public static nfloat GetBaseline (this CoreGraphics.CGRect viewFrame)
{
return 0;
}
/// <summary>
/// The x coordinate of the center of the frame.
/// </summary>
public static nfloat GetCenterX (this CoreGraphics.CGRect viewFrame)
{
return viewFrame.X + viewFrame.Width / 2;
}
/// <summary>
/// The y coordinate of the center of the frame.
/// </summary>
public static nfloat GetCenterY (this CoreGraphics.CGRect viewFrame)
{
return viewFrame.Y + viewFrame.Height / 2;
}
}
}
@praeclarum
Copy link
Author

Here's an example of its use:

    ContentView.ConstrainLayout (() =>

        border.Frame.Top == ContentView.Frame.Top &&
        border.Frame.Height == 0.5f &&
        border.Frame.Left == ContentView.Frame.Left &&
        border.Frame.Right == ContentView.Frame.Right &&

        nameLabel.Frame.Left == ContentView.Frame.Left + hpad &&
        nameLabel.Frame.Right == ContentView.Frame.GetMidX () - 5.5f &&
        nameLabel.Frame.Top >= ContentView.Frame.Top + vpad &&
        nameLabel.Frame.Bottom <= ContentView.Frame.Bottom - vpad &&
        nameLabel.Frame.GetMidY () == ContentView.Frame.GetMidY () &&

        gestureView.Frame.Left <= ContentView.Frame.GetMidX () - 22 &&
        gestureView.Frame.Right >= scalarLabel.Frame.Right + 22 &&
        gestureView.Frame.Width >= 88 &&
        gestureView.Frame.Top == ContentView.Frame.Top &&
        gestureView.Frame.Bottom == ContentView.Frame.Bottom &&

        scalarLabel.Frame.Bottom == ContentView.Frame.Bottom - vpad &&
        scalarLabel.Frame.Left == nameLabel.Frame.Right + 11 &&
        scalarLabel.Frame.Right <= ContentView.Frame.Right - hpad &&
        scalarLabel.Frame.GetMidY () == ContentView.Frame.GetMidY () &&

        unitsLabel.Frame.Left == scalarLabel.Frame.Right + 1 &&
        unitsLabel.Frame.GetBaseline () == scalarLabel.Frame.GetBaseline ()

    );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment