Monday, July 22, 2013

Playing with Constraints - FluentConstraints and FluentLayouts for Xamarin.iOS

Update: code now fully shared at https://github.com/slodge/Cirrious.FluentLayout


If you've watched any of the N+1 series - http://mvvmcross.wordpress.com/ - then you'll no doubt have seen me writing a lot of repetitive, error-prone layout code like:


       var textView = new UITextField(new RectangleF(10, 100, 300, 30));
       Add(textView);
       textView.InputView = picker;
       var label = new UILabel(new RectangleF(10, 130, 300, 30));
       Add(label);


All of this repetitive, error-prone layout code was... of course... unnecessary.  The problem was that I am a dinosaur and sometimes it takes me time to learn what I should be doing...


iOS6 is now almost a year old - and part of iOS6 was a new layout system called constraints. The basic idea behind these constraints is that it allows you to specify relationships between the layouts of UIView objects and their attribute values- so that you can, for example, ask one view to set its Top equal to the Bottom of another view. When you do this, then iOS/UIKit will then try to work out the layout for you at runtime.


I've been playing with these today and they are fabulous - especially when coupled with the power of C# - expect to see more of them in my demos soon!


One gist of code that really makes this lovely is Frank's Easy Layout DSL - see http://praeclarum.org/post/45690317491/easy-layout-a-dsl-for-nslayoutconstraint



This expression based library let's you use simple C# statements to define your layout - it's best summarised by code - see his picture which shows how to layout a button and a text box:





For my experiments I decided to see if I could create a Fluent-style API for the same type of effect. I've nothing against the 'Easy Layout DSL' - I just wanted to learn the constraints for myself, plus I wanted to see if using a Fluent approach gave me more composability and reusability.


What I wanted to do was to see if I could define Frank's 'text and button' layout using Fluent code like:


            View.AddConstraints(
                    button.AtTopOf(View).Plus(vPadding),
                    button.AtRightOf(View).Minus(hPadding),
                    button.Width().EqualTo(ButtonWidth),
 
                    text.AtLeftOf(View, hPadding),
                    text.ToLeftOf(button, hPadding),
                    text.WithSameTop(button)
                );


It turned out that it took a bit longer than I had hoped - there were a few gotchas along the way, mainly to do with "TranslateAutoresizingMaskIntoConstraints" - but within a couple of hours I had this working :)


And once I had that working, I then started to play....


What would a form layout look like?

View.AddConstraints(
 
    fNameLabel.AtTopOf(View, vMargin),
    fNameLabel.AtLeftOf(View, hMargin),
    fNameLabel.ToLeftOf(sNameLabel, hMargin),
 
    sNameLabel.WithSameTop(fNameLabel),
    sNameLabel.AtRightOf(View, hMargin),
    sNameLabel.WithSameWidth(fNameLabel),
 
    fNameField.WithSameWidth(fNameLabel),
    fNameField.WithSameLeft(fNameLabel),
    fNameField.Below(fNameLabel, vMargin),
 
    sNameField.WithSameLeft(sNameLabel),
    sNameField.WithSameWidth(sNameLabel),
    sNameField.WithSameTop(fNameField),
 
    numberLabel.WithSameLeft(fNameLabel),
    numberLabel.ToLeftOf(streetLabel, hMargin),
    numberLabel.Below(fNameField, vMargin),
    numberLabel.WithRelativeWidth(streetLabel, 0.3f),
 
    streetLabel.WithSameTop(numberLabel),
    streetLabel.AtRightOf(View, hMargin),
 
    numberField.WithSameLeft(numberLabel),
    numberField.WithSameWidth(numberLabel),
    numberField.Below(numberLabel, vMargin),
 
    streetField.WithSameLeft(streetLabel),
    streetField.WithSameWidth(streetLabel),
    streetField.WithSameTop(numberField),
 
    townLabel.WithSameLeft(fNameLabel),
    townLabel.WithSameRight(streetLabel),
    townLabel.Below(numberField, vMargin),
 
    townField.WithSameLeft(townLabel),
    townField.WithSameWidth(townLabel),
    townField.Below(townLabel, vMargin),
 
    zipLabel.WithSameLeft(fNameLabel),
    zipLabel.WithSameWidth(townLabel),
    zipLabel.Below(townField, vMargin),
 
    zipField.WithSameLeft(townLabel),
    zipField.WithSameWidth(zipLabel),
    zipField.Below(zipLabel, vMargin),
 
    debug.WithSameLeft(townLabel),
    debug.WithSameWidth(zipLabel),
    debug.AtBottomOf(View, vMargin)
 
); 
 
... although I think there are some opportunities to shorten that code and perhaps also to use some code-based hints too!


Could I create a generic vertical scrolling StackPanel/LinearLayout?


 public static IEnumerable<FluentLayout> 
       VerticalStackPanelConstraints(
          this UIView parentView, 
          Margins margins,
          params UIView[] views)
 {
     margins = margins ?? new Margins();
 
     UIView previous = null;
     foreach (var view in views)
     {
        yield return view.Left()
                         .EqualTo()
                         .LeftOf(parentView)
                         .Plus(margins.Left);
        yield return view.Width()
                         .EqualTo()
                         .WidthOf(parentView)
                         .Minus(margins.Right + margins.Left);
        if (previous != null)
           yield return view.Top()
                            .EqualTo()
                            .BottomOf(previous)
                            .Plus(margins.Top);
        else
           yield return view.Top()
                            .EqualTo()
                            .TopOf(parentView)
                            .Plus(margins.Top);
        previous = view;
     }
     if (parentView is UIScrollView)
        yield return previous.Bottom()
                             .EqualTo()
                             .BottomOf(parentView)
                             .Minus(margins.Bottom);
 }


Adaptive!

One key thing to note about these constraint-based UIs is that they are adaptive - e.g. when you rotate the phone then the layout adapts:

 




The code

The code I created is currently sitting in https://github.com/slodge/MvvmCross-Tutorials/tree/master/QuickLayout/Cirrious.FluentLayout - along with a test MvvmCross project (one level up).


It may later move into an MvvmCross plugin - or into core MvvmCross - but for now it's just sitting there in Tutorials. License is Ms-PL as per normal.


A video demo - laying out a tipcalc view





More?

With all this said and done, whether or not you prefer declarative or Imperative UI code is very much a matter of taste... but one question that I'm wondering at the moment is whether I could use the same UI code to create layouts in different environments - whether the same `AtTopOf`, `ToLeftOf` type calls could be used to generate UIKit, Xaml or Axml... but that question will have to wait for another day....

15 comments:

  1. Stuart, I am sure you have seen XibFree - http://www.toptensoftware.com/XibFree/

    For simple layouts it works great.

    ReplyDelete
    Replies
    1. Yes - thanks - I've also mentioned it before too - e.g. in my NDC talk. Very happy to see more and more happening in this area - I've got to stop laying my iOS UIs out by hand :)

      Delete
  2. Please submit this to the Xamarin Component Store!

    ReplyDelete
    Replies
    1. Thanks David

      Would be interested in submitting this after mvvmcross - but can't see any store support for portable code currently :/ is this likely soon? Do you think it can even done now?

      Stuart

      Delete
  3. The holy grail is a cross-platform tool. I'm sure we agree. It should offer layouts syncing imperative (C#), declarative (XAML), and visual. For Visual Studio devs, I can't see anything but a WPF based tool that emits cross-platform.

    Closest I've found is Appcelerator's Alloy. http://www.appcelerator.com/platform/alloy/

    ReplyDelete
  4. Just finished migrating my code-only layouts from XibFree to AutoLayout using your FluentLayout extension. Around 50% less LoC and much more readability!

    And finally got rid of XibFree NativeView subclasses.

    ReplyDelete
  5. Really great Job, thank you.
    But Is there a way to remove a constraint by using the fluentlayout-library?

    I have already tried it with this code, but it doesn't work:

    this.View.Superview.AddConstraints(this.View.AtLeftOf(this.View.Superview).ToLayoutConstraints().ToArray());

    this.View.Superview.RemoveConstraints(this.View.AtLeftOf(this.View.Superview).ToLayoutConstraints().ToArray());

    ReplyDelete
    Replies
    1. Were you ever able to remove a constraint?

      Delete
  6. Microsoft's Windows 10 Relative Panels for xaml look verrrry similar to what you have written here http://channel9.msdn.com/Series/Developers-Guide-to-Windows-10-Preview/08

    ReplyDelete
  7. This is wonderful and definitely the quickest way of writing layouts. Thank you!

    ReplyDelete
  8. Stuart, been using this library for a few years now, thanks. But recently ive needed to specify multiplier values. Can multiplier values be specified somehow?

    im trying to set a view to have a width of less than or equal to 75% of its parent view. e.g. an equal width constraint to its parent but with a multiplier of 0.75.

    ReplyDelete
  9. I managed this Stuart, I wasn't aware of the WithMultiplier method. I wrote this to achieve it:

    view.Width().GreaterThanOrEqualTo(0).WidthOf(view.Superview).WithMultiplier(75 / 100).SetPriority(1000)

    ReplyDelete
    Replies
    1. phew... sorry.. had been meaning to reply... in addition to WithMultiplier, I think some of the fluent methods also used to take optional multiplier weight parameters... but they may have since disappeared (Greg and others are doing a fab job keeping this project fluidly moving forwards)

      Delete
  10. correction to previous comment, it should have said LessThanOrEqualTo:

    view.Width().LessThanOrEqualTo(0).WidthOf(view.Superview).WithMultiplier(75 / 100).SetPriority(1000)

    ReplyDelete
  11. hello, I got a question, I have something like this:

    an image centered and below it, and small image of 30 x30 and a textfield,
    I can center the elements in a scrollview and works great, but i can't manage to resize the textfield when rotation occurs i want 30 pixels to the right and 30 pixels to the small image

    my constrains are as follow:


    centeredimage.WithSameCenterX(_scrollView),
    centeredimage.AtTopOf(_scrollView, UIApplication.SharedApplication.StatusBarFrame.Height),
    centeredimage.Width().EqualTo(100),
    centeredimage.Height().EqualTo(100),

    imageLogin.Below(centeredimage, padding),
    imageLogin.AtLeftOf(_scrollView,50),
    imageLogin.Height().EqualTo(30),
    imageLogin.Width().EqualTo(30)

    inputLogin ????

    any idea on what contrains are needed??


    ReplyDelete