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