Skip to content

Instantly share code, notes, and snippets.

@praeclarum
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 |> Seq.map eval |> Seq.toArray)
| PropertyGet (Some o, p, i) -> p.GetValue (eval o, i |> Seq.map 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
cs
@praeclarum
Copy link
Author

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 ()
        true

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

@dvdsgl
Copy link

dvdsgl commented Jul 1, 2014

👏

@7sharp9
Copy link

7sharp9 commented Jul 2, 2014

If only there were string to quotation functions available.

@TIHan
Copy link

TIHan commented Jul 2, 2014

Great job! :)

@dd105
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#?

@7sharp9
Copy link

7sharp9 commented Jul 3, 2014

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

@polytypic
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.

@7sharp9
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.

@vasily-kirichenko
Copy link

I think it's better to rewrite

let (|GetFrameProp|) e = ...

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

with

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

match side with
| GetFrameProp x -> ...

@bentayloruk
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() =
    base.ViewDidLoad()

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

    // Create a sub content view.
    let contentView = new UIView(BackgroundColor = UIColor.LightGray)
    rootView.AddSubview(contentView)
    rootView.ConstrainLayout(
        <@[| 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?

@robkuz
Copy link

robkuz commented Dec 3, 2014

awesome!

@hussam
Copy link

hussam commented Mar 26, 2015

Awesome Stuff! I updated it to work with the Unified API: https://gist.github.com/hussamal/0127e676a789fc704d8e

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