Last active
August 23, 2020 09:52
-
-
Save praeclarum/d7dc5d83bacf84c127c4 to your computer and use it in GitHub Desktop.
EasyLayout makes writing auto layout code in Xamarin.iOS F# easier.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
@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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.