Skip to content

Instantly share code, notes, and snippets.

Last active August 23, 2020 09:52
Show Gist options
  • Save praeclarum/d7dc5d83bacf84c127c4 to your computer and use it in GitHub Desktop.
Save praeclarum/d7dc5d83bacf84c127c4 to your computer and use it in GitHub Desktop.
EasyLayout makes writing auto layout code in Xamarin.iOS F# easier.
module EasyLayout
open System
open System.Drawing
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns
open MonoTouch.Foundation
open MonoTouch.UIKit
module private Utilities =
let rec eval e =
match e with
| FieldGet (Some o, f) -> f.GetValue (eval o)
| FieldGet (None, f) -> f.GetValue (null)
| PropertyGet (None, p, i) -> p.GetValue (null, i |> eval |> Seq.toArray)
| PropertyGet (Some o, p, i) -> p.GetValue (eval o, i |> eval |> Seq.toArray)
| Value (x, _) -> x
| _ -> raise (Exception (sprintf "Don't know how to eval %A" e))
let toAttr m =
match m with
| "X" | "Left" -> NSLayoutAttribute.Left
| "Y" | "Top" -> NSLayoutAttribute.Top
| "Width" -> NSLayoutAttribute.Width
| "Height" -> NSLayoutAttribute.Height
| "Bottom" -> NSLayoutAttribute.Bottom
| "Right" -> NSLayoutAttribute.Right
| "CenterX" | "RectangleF.get_CenterX" -> NSLayoutAttribute.CenterX
| "CenterY" | "RectangleF.get_CenterY" -> NSLayoutAttribute.CenterY
| "Baseline" | "RectangleF.get_Baseline" -> NSLayoutAttribute.Baseline
| _ -> NSLayoutAttribute.NoAttribute
let isConstrainableProperty m = toAttr m <> NSLayoutAttribute.NoAttribute
let (|GetFrameProp|) e =
match e with
| Let (_, PropertyGet (Some o, fn, _), PropertyGet (_, pn, _))
when fn.Name = "Frame" && isConstrainableProperty pn.Name ->
Some (eval o :?> NSObject, toAttr pn.Name)
| Call (_, pn, [PropertyGet (Some o, fn, _)])
when fn.Name = "Frame" && isConstrainableProperty pn.Name ->
Some (eval o :?> NSObject, toAttr pn.Name)
| _ -> None
let compileLeftSide side =
match side with
| GetFrameProp (Some x) -> x
| _ -> raise (Exception (sprintf "Left hand side of constraint is expected to be a UIView.Frame property. It was: %A" side))
let (|Mul|) side =
match side with
| GetFrameProp (Some (x, p)) -> Some (x, p, 1.0f)
| Call (_, m, [l; GetFrameProp (Some (x, p))]) when m.Name = "op_Multiply" -> Some (x, p, Convert.ToSingle (eval l))
| Call (_, m, [GetFrameProp (Some (x, p)); l]) when m.Name = "op_Multiply" -> Some (x, p, Convert.ToSingle (eval l))
| _ -> None
let compileRightSide side =
match side with
| Mul (Some x) -> (Some x, 0.0f)
| Call (_, mem, [Mul (Some x); c]) when mem.Name = "op_Addition" -> (Some x, Convert.ToSingle (eval c))
| Call (_, mem, [Mul (Some x); c]) when mem.Name = "op_Subtraction" -> (Some x, -Convert.ToSingle (eval c))
| Value (x, _) -> (None, Convert.ToSingle (x))
| FieldGet _ -> (None, Convert.ToSingle (eval side))
| _ -> raise (Exception (sprintf "Unrecognized right hand side: %A." side))
let compileConstraint left right rel =
let (firstObj, firstAttr) = compileLeftSide left
let (maybeObj, add) = compileRightSide right
match maybeObj with
| None -> NSLayoutConstraint.Create (firstObj, firstAttr, rel, null, NSLayoutAttribute.NoAttribute, 0.0f, add)
| Some (secObj, secAttr, mul) -> NSLayoutConstraint.Create (firstObj, firstAttr, rel, secObj, secAttr, mul, add)
let toRel m =
match m with
| "op_Equality" -> Some NSLayoutRelation.Equal
| "op_LessThanOrEqual" -> Some NSLayoutRelation.LessThanOrEqual
| "op_GreaterThanOrEqual" -> Some NSLayoutRelation.GreaterThanOrEqual
| _ -> None
let rec compileConstraints expr =
match expr with
| NewArray (_, es) -> es |> Seq.collect compileConstraints |> Seq.toList
| IfThenElse (i, t, e) -> compileConstraints i @ compileConstraints t @ compileConstraints e
| Call (_, m, [l; r]) when (toRel m.Name).IsSome -> [compileConstraint l r (toRel m.Name).Value]
| Value _ -> []
| _ -> raise (Exception (sprintf "Unable to recognize constraints in expression: %A" expr))
type RectangleF with
member this.CenterX = this.X + this.Width / 2.0f
member this.CenterY = this.Y + this.Height / 2.0f
member this.Baseline = 0.0f
type UIView with
/// <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>
/// <code><@ button.Frame.Left &gt;= text.Frame.Right + 22 &amp;&amp;
/// button.Frame.Width = View.Frame.Width * 0.42f @></code>
/// </summary>
/// <param name="constraints">Constraint equations and inequalities.</param>
member this.ConstrainLayout (constraints) =
let cs = Utilities.compileConstraints constraints |> Seq.toArray
this.AddConstraints (cs)
for x in cs do
(x.FirstItem :?> UIView).TranslatesAutoresizingMaskIntoConstraints <- false
Copy link

This code makes writing Auto Layout constraints using F# easier. Simply call UIView.ConstrainLayout to create the constraints:

open System
open MonoTouch.UIKit
open MonoTouch.Foundation

open EasyLayout

[<Register ("AppDelegate")>]
type AppDelegate () =
    inherit UIApplicationDelegate ()

    let window = new UIWindow (UIScreen.MainScreen.Bounds)

    let root = new UIViewController ()

    let a = new UIView (BackgroundColor = UIColor.Red)
    let b = new UIView (BackgroundColor = UIColor.Green)
    let c = new UIView (BackgroundColor = UIColor.Blue)

    override this.FinishedLaunching (app, options) =

        root.View.AddSubviews (a, b, c)

        root.View.ConstrainLayout (
                a.Frame.Top = root.View.Frame.Top
                a.Frame.Height = 100.0f
                a.Frame.Left = root.View.Frame.Left
                a.Frame.Right = root.View.Frame.Right

                b.Frame.CenterX = a.Frame.CenterX
                b.Frame.Width = a.Frame.Width * 0.5f
                b.Frame.Top = a.Frame.Bottom + 10.0f
                b.Frame.Bottom = root.View.Frame.CenterY

                c.Frame.Width = b.Frame.Width * 1.25f
                c.Frame.Left = b.Frame.Left
                c.Frame.Top = b.Frame.Bottom + 10.0f
                c.Frame.Bottom = root.View.Frame.Bottom
            |]@>) |> ignore

        window.RootViewController <- root
        window.MakeKeyAndVisible ()

The C# version of EasyLayout is about 3 times longer than this version. Go F#!

Updated 7/2 Updated to have cleaner syntax thanks to @7sharp9

Copy link

dvdsgl commented Jul 1, 2014


Copy link

7sharp9 commented Jul 2, 2014

If only there were string to quotation functions available.

Copy link

TIHan commented Jul 2, 2014

Great job! :)

Copy link

dd105 commented Jul 3, 2014

I'm curious: since the C# version was created a while ago (and presumably like the rest of us your code skill has improved in the meantime), would you think that it's possible to re-work the C# version so it's not so verbose compared to F#? Or are we condemned to always writing about 3x more code if we write C#?

Copy link

7sharp9 commented Jul 3, 2014

In C# you are always condemned to writing more code.

Copy link

Hmm... I wonder whether it is necessary to use quotations. It might be possible to achieve similar structuring of the desired constraints using a combinator library rather than quotation interpretation. The main difficulty with a typical interpreter approach is that the interpreter is one closed piece of functionality and to extend it, you need to modify the interpreter. The interpreter then accumulates complexity and becomes a code structuring bottleneck. In the example, you are using individual properties (Width, Top, and so on). Suppose you wish to express more complicated constraints that deal with multiple properties at once. With a combinator library, the user of the constraint mechanism could likely easily write such more complicated constraints as ordinary F# expressions and use those.

Copy link

7sharp9 commented Jul 4, 2014

@VesaKarvonen Thats why I mentioned the custom symbol earlier on, that way you could use infix notation for the constraints still. You could also use a computation expression to hide the complexity of the combinators too.

Copy link

I think it's better to rewrite

let (|GetFrameProp|) e = ...

match side with
| GetFrameProp (Some x) -> ...


let (|GetFrameProp|_|) e = ...

match side with
| GetFrameProp x -> ...

Copy link

Thanks for building this. I'm keen to keep my auto-layout code tight!

I'm trying EasyLayout and it works fine on the simulator. However, I am getting an exception when debugging on a real iPhone. The code that is crashing is as follows:

override __.ViewDidLoad() =

    let rootView = __.View
    rootView.BackgroundColor <- UIColor.LightGray

    // Create a sub content view.
    let contentView = new UIView(BackgroundColor = UIColor.LightGray)
        <@[| contentView.Frame.Top = rootView.Frame.Top + 45.0f
             contentView.Frame.Left = rootView.Frame.Left
             contentView.Frame.Right = rootView.Frame.Right
             contentView.Frame.Bottom = rootView.Frame.Bottom |]@>) |> ignore

The exception in questions is as follows:

Failed to bind property 'Frame'. Parameter name: propName.

Anybody experienced this?

Copy link

robkuz commented Dec 3, 2014


Copy link

hussam commented Mar 26, 2015

Awesome Stuff! I updated it to work with the Unified API:

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