-
-
Save praeclarum/d7dc5d83bacf84c127c4 to your computer and use it in GitHub Desktop.
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 && operator.</para> | |
/// <code><@ button.Frame.Left >= text.Frame.Right + 22 && | |
/// 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 |
👏
If only there were string to quotation functions available.
Great job! :)
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#?
In C# you are always condemned to writing more code.
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.
@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.
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 -> ...
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?
awesome!
Awesome Stuff! I updated it to work with the Unified API: https://gist.github.com/hussamal/0127e676a789fc704d8e
This code makes writing Auto Layout constraints using F# easier. Simply call UIView.ConstrainLayout to create the constraints:
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