When developing Views in iOS, it's inevitable that you're going to need to setup constraints for your UI objects. This is somewhat trivial when designing your views using the visual designer. However, when setting up views programmatically, you need to rely on the AutoLayout NSLayoutConstraint object to do this for you.

Loading Message with Constraints

Let's take Xamarin's Loading Message example and augment it to take advantage of the NSLayoutConstraint class instead of using absolute positioning.

Xamarin's Example:

public class LoadingOverlay : UIView {  
    // control declarations
    UIActivityIndicatorView activitySpinner;
    UILabel loadingLabel;

    public LoadingOverlay (RectangleF frame) : base (frame)
    {
        // configurable bits
        BackgroundColor = UIColor.Black;
        Alpha = 0.75f;
        AutoresizingMask = UIViewAutoresizing.FlexibleDimensions;

        float labelHeight = 22;
        float labelWidth = Frame.Width - 20;

        // derive the center x and y
        float centerX = Frame.Width / 2;
        float centerY = Frame.Height / 2;

        // create the activity spinner, center it horizontall and put it 5 points above center x
        activitySpinner = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.WhiteLarge);
        activitySpinner.Frame = new RectangleF (
            centerX - (activitySpinner.Frame.Width / 2) ,
            centerY - activitySpinner.Frame.Height - 20 ,
            activitySpinner.Frame.Width ,
            activitySpinner.Frame.Height);
        activitySpinner.AutoresizingMask = UIViewAutoresizing.FlexibleMargins;
        AddSubview (activitySpinner);
        activitySpinner.StartAnimating ();

        // create and configure the "Loading Data" label
        loadingLabel = new UILabel(new RectangleF (
            centerX - (labelWidth / 2),
            centerY + 20 ,
            labelWidth ,
            labelHeight
            ));
        loadingLabel.BackgroundColor = UIColor.Clear;
        loadingLabel.TextColor = UIColor.White;
        loadingLabel.Text = "Loading Data...";
        loadingLabel.TextAlignment = UITextAlignment.Center;
        loadingLabel.AutoresizingMask = UIViewAutoresizing.FlexibleMargins;
        AddSubview (loadingLabel);
    }

    /// <summary>
    /// Fades out the control and then removes it from the super view
    /// </summary>
    public void Hide ()
    {
        UIView.Animate (
            0.5, // duration
            () => { Alpha = 0; },
            () => { RemoveFromSuperview(); }
        );
    }
};

The example uses RectangleF to set the Frame of the UI elements. This forces the UI objects to position themselves absolutely within their containing View. This method involves a little bit of math to determine where the UI elements should position themselves. In this example the labelElement and activitySpinner object are centered and stacked over one another.

Let's take this example and augment it to implement constraints.

I'd like to take two approaches to this: One example will be to implement the stacked Loading Data... and spinner. The other will be to display the Loading Data... and spinner side-by-side, centered on the screen.

Clean up

First things first, let's cleanup the implementation. We'll create some properties that control the state and size of the UI elements within the LoadingOverlay view.

    public class LoadingOverlay : UIView {
        // control declarations

        UIActivityIndicatorView ActivitySpinner;
        UILabel LoadingLabel;

        int SpinnerWidth = 20;
        int SpinnerHeight = 20;

        // Will control the margin between the label and spinner
        int margin = 5;

        public LoadingOverlay () : base (UIScreen.MainScreen.Bounds)
        {
            // Set the background transparency
            BackgroundColor = UIColor.FromRGBA (0f, 0f, 0f, 0.65f);
            AutoresizingMask = UIViewAutoresizing.FlexibleDimensions;

            // create the activity spinner
            ActivitySpinner = new UIActivityIndicatorView (UIActivityIndicatorViewStyle.White) {

                // Important when using AutoLayout
                TranslatesAutoresizingMaskIntoConstraints = false,
            };

            // create and configure the "Loading Data" label
            LoadingLabel = new UILabel (){ 

                BackgroundColor = UIColor.Clear,
                TextColor = UIColor.White,
                Text = "Loading Data...",
                TextAlignment = UITextAlignment.Center,

                // Important when using AutoLayout
                TranslatesAutoresizingMaskIntoConstraints = false
            };

            ActivitySpinner.StartAnimating ();

            AddSubviews (new UIView[]{ ActivitySpinner, LoadingLabel });

            // Important part of setting up constraints. 
            // Will call UpdateConstraints on the base implementation.
            //
            SetNeedsUpdateConstraints ();
        }
}

The constructor will create our UI objects and most importantly, prepares them for AutoLayout by setting the TranslatesAutoresizingMaskIntoConstraints to false. We then add the UI Objects to the View.

Finally, we invoke the SetNeedsUpdateConstraints() method. This part is key, since it will force the view to call it's UpdateConstraints() method.

Let's override the base implementation and add our own routine.

        public override void UpdateConstraints ()
        {
            // Check to see if the View needs to update it's constraints
            if(NeedsUpdateConstraints()) {
                setupConstraints(); // Setup our constraints
            }
            base.UpdateConstraints (); // Important, always call this last
        }

The UpdateConstraints method will check to see if the View constraints have been set, if not, we'll set them by calling a private method setupConstraints() which we'll look at in just a moment.

The last step is to call the super base.UpdateConstraints() method, specified in Apple's iOS documentation.

IMPORTANT

Call [super updateConstraints] as the final step in your implementation.

Let's work on the setupConstraints implementation.

#region PrivateMethods

        void setupConstraints() {
        // Setup some constraints
        // AddConstraints(constraints);
        }

#endregion

This is our base implementation. Let's start building our layout constraints.

First things first, we need to setup our list of constraints and some view metrics. The list of constraints will be the collection we'll pass to the AddConstraints method which expects a collection of NSLayoutConstraint.

        void setupConstraints() {

            List<NSLayoutConstraint> constraints = new List<NSLayoutConstraint> ();

            // Views and Metrics (tokens) we'll be passing to the Visual Format
            var viewMetrics = new Object[] { 
                "label", LoadingLabel,
                "spinner", ActivitySpinner,
                "spinnerHeight", SpinnerHeight,
                "spinnerWidth", SpinnerWidth,
                "margin", margin
            };
        }

The important piece to note here is that we're creating a collection of combined views and metrics. These will be used as tokens when using the Visual Format Language (VFL).

VFL allows us to define multiple constraints in one line. It is important to note that we'll need to think of visual constraints as Rows (H:) and Columns(V:).

We'll be taking advantage of the NSLayoutConstraint.FromVisualFormat() method to accomplish our constraints.

We're going to pass several parameters to the method:

  • Format parameter, specified with a string.
  • NSLayoutFormatOptions to specify the allignment rules
  • views and metrics, which we'll pass in as a combined collection.

The function will return a NSLayoutConstraint[] collection, which we'll add to our constraints List instance.

Stacked Loading

The current Xamarin implementation stacks the Data Loading... text over the Spinner.

Let's write that implementation using the NSLayoutConstraint object.

        void setupConstraints() {

            List<NSLayoutConstraint> constraints = new List<NSLayoutConstraint> ();

            // Views and Metrics (tokens) we'll be passing to the Visual Format
            var viewMetrics = new Object[] { 
                "label", LoadingLabel,
                "spinner", ActivitySpinner,
                "spinnerHeight", SpinnerHeight,
                "spinnerWidth", SpinnerWidth,
                "margin", margin
            };


            // First specify the Vertical Rule 
            // [Label]
            // -- margin --
            // [Spinner]
            //
            constraints.AddRange(
                NSLayoutConstraint.FromVisualFormat(
                    "V:[label]-margin-[spinner]", 
                    NSLayoutFormatOptions.AlignAllCenterX, 
                    viewMetrics
                )
            ); 

            // Add the constraints to the view
            AddConstraints (constraints.ToArray ());

        }

The first set of constraints have been specified! The label and spinner will appear stacked over one another, seperated by a 5 pixel margin. The important factor to note is the V: specifier, this tells the Visual format that we're settinga Vertical rule for this constraint set. The FormatOption will tell the constraint to center the two UI objects (label and spinner). However, both items will still render at the x:0 and y:0 coordinate, essentially sticking them in the uppper left corner of the UI.

First stacked example

Let's work on centering the to their parent view. This time we'll implement the NSLayoutConstraint.Create method. I had a hell of a time figuring out how to do this visually, but it seemed a bit difficult to accomplish (please correct me if i'm wrong!). The NSLayoutConstraint.Create method allows us to accomplish this fairly easily.

...
        void setupConstraints() {

            List<NSLayoutConstraint> constraints = new List<NSLayoutConstraint> ();

            // Views and Metrics (tokens) we'll be passing to the Visual Format
            var viewMetrics = new Object[] { 
                "label", LoadingLabel,
                "spinner", ActivitySpinner,
                "spinnerHeight", SpinnerHeight,
                "spinnerWidth", SpinnerWidth,
                "margin", margin
            };


            // First specify the Vertical Rule 
            // [Label]
            // -- margin --
            // [Spinner]
            //
            constraints.AddRange(
                NSLayoutConstraint.FromVisualFormat(
                    "V:[label]-margin-[spinner]", 
                    NSLayoutFormatOptions.AlignAllCenterX, 
                    viewMetrics
                )
            );

            // Align the LoadingLabel with the View's CenterX position. 
            // Because of the constraint rule above, the spinner will be centered as well.
            //
            // Align the LoadingLabel with the View's CenterX position. 
            // Because of the constraint rule above, the spinner will be centered as well.
            //
            constraints.Add (
                NSLayoutConstraint.Create (
                    LoadingLabel, // View we want to constraint
                    NSLayoutAttribute.CenterX, // Vertically Center
                    NSLayoutRelation.Equal, // Relationship
                    this, // View we want to constrain to
                    NSLayoutAttribute.CenterX, // Attribute to constrain to
                    1, // Multiplier
                    0 // constant
                )
            );


            // Add the constraints to the view
            AddConstraints (constraints.ToArray ());

        }

This will center the LoadingLabel objec to the View's center x position. Because we specified a constraint for the label and spinner object's vertical x position, this in turn will pull the spinner to the center as well.

Stacked and Centered UI example

Great! Both items are centered vertically. Let's work on centering them Horizontally next.

We'll take advantage of the NSLayoutConstraint.Create method again.

        void setupConstraints() {

            List<NSLayoutConstraint> constraints = new List<NSLayoutConstraint> ();

            // Views and Metrics (tokens) we'll be passing to the Visual Format
            var viewMetrics = new Object[] { 
                "label", LoadingLabel,
                "spinner", ActivitySpinner,
                "spinnerHeight", SpinnerHeight,
                "spinnerWidth", SpinnerWidth,
                "margin", margin
            };


            // First specify the Vertical Rule 
            // [Label]
            // -- margin --
            // [Spinner]
            //
            constraints.AddRange(
                NSLayoutConstraint.FromVisualFormat(
                    "V:[label]-margin-[spinner]", 
                    NSLayoutFormatOptions.AlignAllCenterX, 
                    viewMetrics
                )
            );

            // Align the LoadingLabel with the View's CenterX position. 
            // Because of the constraint rule above, the spinner will be centered as well.
            //
            constraints.Add (
                NSLayoutConstraint.Create (
                    LoadingLabel, // View we want to constraint
                    NSLayoutAttribute.CenterX, // Vertically Center
                    NSLayoutRelation.Equal, // Relationship
                    this, // View we want to constrain to
                    NSLayoutAttribute.CenterX, // Attribute to constrain to
                    1, // Multiplier
                    0 // constant
                )
            );

            // Align the LoadingLabel with the View's CenterY position.
            // We continue to maintain a constraint on the spinner, so it will be centered as well!
            //
            constraints.Add (
                NSLayoutConstraint.Create(
                    LoadingLabel, // View we want to constrain
                    NSLayoutAttribute.CenterY, // Horizontally center
                    NSLayoutRelation.Equal, // Relationship
                    this, // View we want to constrain to
                    NSLayoutAttribute.CenterY, // Horizontally center
                    1, // Multiplier

                    // constant subtract the spinner height from the layout equation.
                    // Basically, pull the LoadingLabel and spinner up 20 pixels
                    -SpinnerHeight 
                )
            );

            // Add the constraints to the view
            AddConstraints (constraints.ToArray ());

        }

The final example centers the LoadingLabel object's CenterY position equal to the containing View's CenterY position. Notice the last parameter passed to the Create method, it's the constant. I wanted to make up for the spinners height, therefore subtracting 20 pixels from the constant parameter. I don't have to do much more math than that, the AutoLayout algorithm will account for that multiplier for any Layout orienation, it's pretty magical!

That's pretty much all it takes for stacking. Let's get to the side-by-side example next.

Side By Side Loading

We're going to augment our example to Layout the Loading Data and spinner message side-by-side.

The updates are actually minimal.

First let's change our UI object's constraints from Vertical (V:) to Horizontal (H:), which is the default value if not specified in the format string.

        void setupConstraints() {

            List<NSLayoutConstraint> constraints = new List<NSLayoutConstraint> ();

            // Views and Metrics (tokens) we'll be passing to the Visual Format
            var viewMetrics = new Object[] { 
                "label", LoadingLabel,
                "spinner", ActivitySpinner,
                "spinnerHeight", SpinnerHeight,
                "spinnerWidth", SpinnerWidth,
                "margin", margin
            };


            // First specify the Horizontal Rule 
            // [Label] -- margin -- // [Spinner]
            //
            constraints.AddRange(
                NSLayoutConstraint.FromVisualFormat(
                    "[label]-margin-[spinner]", 
                    NSLayoutFormatOptions.AlignAllCenterY, 
                    viewMetrics
                )
            );
       }

Didn't take much. The format went from vertical to horizontal, the format option aligns both UI elements horizontally by specifiing the enum value AlignAllCenterY. Notice that we're also spacing the Label and Spinner by the 5 pixels -margin-.

Next we'll need to swap pulling the LoadingLabel over by the spinner with to pulling it up by the spinner height.

        void setupConstraints() {

            List<NSLayoutConstraint> constraints = new List<NSLayoutConstraint> ();

            // Views and Metrics (tokens) we'll be passing to the Visual Format
            var viewMetrics = new Object[] { 
                "label", LoadingLabel,
                "spinner", ActivitySpinner,
                "spinnerHeight", SpinnerHeight,
                "spinnerWidth", SpinnerWidth,
                "margin", margin
            };


            // First specify the Horizontal Rule 
            // [Label] -- margin -- // [Spinner]
            //
            constraints.AddRange(
                NSLayoutConstraint.FromVisualFormat(
                    "[label]-margin-[spinner]", 
                    NSLayoutFormatOptions.AlignAllCenterY, 
                    viewMetrics
                )
            );

            // Align the LoadingLabel with the View's CenterX position. 
            // Because of the constraint rule above, the spinner will be centered as well.
            //
            constraints.Add (
                NSLayoutConstraint.Create (
                    LoadingLabel, // View we want to constraint
                    NSLayoutAttribute.CenterX, // Vertically Center
                    NSLayoutRelation.Equal, // Relationship
                    this, // View we want to constrain to
                    NSLayoutAttribute.CenterX, // Attribute to constrain to
                    1, // Multiplier

                    // pull the label UP by the spinners height
                    -SpinnerHeight 
                )
            );

            // Align the LoadingLabel with the View's CenterY position.
            // We continue to maintain a constraint on the spinner, so it will be centered as well!
            //
            constraints.Add (
                NSLayoutConstraint.Create(
                    LoadingLabel, // View we want to constrain
                    NSLayoutAttribute.CenterY, // Horizontally center
                    NSLayoutRelation.Equal, // Relationship
                    this, // View we want to constrain to
                    NSLayoutAttribute.CenterY, // Horizontally center
                    1, // Multiplier
                    0 // constant
                )
            );

            // Add the constraints to the view
            AddConstraints (constraints.ToArray ());

        }

This will result in the LoadingLabel and ActivityIndicator to be displayed side-by-side centered in the View.

Side by side Loading indicator Side by side Loading indicator Landscape

Finally

I hope this demonstrate the benefits of allowing the NSLayoutConstraint class to do the layout thinking for you.

The Visual Format Language is very helpful when one needs to create a number of constraints. It does have it's limitations concerning setting properties or performing mathmatical operations within the expression. To me, the benefits outweigh the limitations, I can perform my mathmatical operations outside the VFL and pass in the operation results via the metrics collection.

Suggestions

Suggestions are welcomed. Please submit an issue on the Demo Project.

Demo

Demo available on Github.

Helpful Resources