Dynamic Constraint Calculator for iOS

In today’s world of iOS development, graphical interface editors, especially Storyboards, gain more and more popularity. It’s because they continously improve, are easy to grasp, and provide a lot of features to make our lives easier. Not to mention, they save us from a ton of coding.

One of these key features is Auto Layout, the constraint-based layout system that was introduced years ago (I don’t think this is surprising or new to anyone). While it’s true that constraints can be created and managed programmatically, let’s face it: It’s a huge pain in the rear. The solution sounds easy, let’s use the graphical layout editor! However, when the layout needs changing, there are a few shortcomings in terms of flexibility .

There is a multitude of ways to keep our interfaces manageable. In this article, we’re going to take a look at a new approach to the flexibility of constraints. So whether you are a complete beginner at Auto Layout or not, read on!

text

Problems ahead

Let’s say we need to create an app with multiple screens, all using Auto Layout. We add the UI elements and set up a dozen spacing and size constraints in all of the ViewControllers.

Now imagine that we (or heavens forbid, the client) change our minds and want all of the similar properties (like vertical spacing) changed. Now we have to readjust all of our carefully laid out constraints.

The easiest solutions

The “tunnel vision”

This is what you shouldn’t do (although we’ve all done it at one point of our lives): Greedily going through your ViewControllers, selecting all the constraints in question, and assigning the new values to the constants. Congratulations, you just spent about 10 minutes with something that you might have to repeat several times by the time your app looks satisfactory. This process is time consuming, prone to a lot of human errors (selecting the wrong constraint, accidentally changing something that we didn’t want to, formatting our entire hard drive, you name it).

The “outlet hell”

A lot smarter approach is to connect your constraints to their respective ViewControllers via outlets, and assign their values at runtime. This is easiest done with the Assistant Editor of XCode, drag-n-dropping your UI elements into your code, until you end up with something like this (note the name of the paragraph):

@IBOutlet weak var spacingConstraint1: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint2: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint3: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint4: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint5: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint6: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint7: NSLayoutConstraint!
    @IBOutlet weak var spacingConstraint8: NSLayoutConstraint!
  
    override func viewDidLoad() {
        super.viewDidLoad()       
 
        spacingConstraint1.constant = 20
        spacingConstraint2.constant = 20
        spacingConstraint3.constant = 30
        spacingConstraint4.constant = 30
        spacingConstraint5.constant = 30
        spacingConstraint6.constant = 15
        spacingConstraint7.constant = 13 // Don't judge me, this sort of thing happens
        spacingConstraint8.constant = 15
    }

Making changes in our layout is a lot easier now in terms of time, but that’s not quite what we like to see in our classes, is it? Seemingly redundant assignments, “naked” integers, unnecessary lines of code.

To be fair, there are times when this solution isn’t that horrible, e.g. in case you have to apply some mathematical logic to them, but still, there is a lot of room for improvement.

A small step for mankind

The first improvement is not really black magic. Creating classes that handle constant values is generally considered a good practice, and the first step should be definitely this one. Let’s make a class called Constants, and set up a few values:

import UIKit
  
class Constants{
    static let spacingType1: CGFloat = 20;
    static let spacingType2: CGFloat = 30;
    static let spacingType3: CGFloat = 15;
    static let spacingType4: CGFloat = 13;
}

Now that we have these values stored independently from the view hierarchy, we can make some smarter changes to our ViewController(s):

override func viewDidLoad() {
    super.viewDidLoad()       
 
    spacingConstraint1.constant = Constants.spacingType1
    spacingConstraint2.constant = Constants.spacingType1
    spacingConstraint3.constant = Constants.spacingType2
    spacingConstraint4.constant = Constants.spacingType2
    spacingConstraint5.constant = Constants.spacingType2
    spacingConstraint6.constant = Constants.spacingType3
    spacingConstraint7.constant = Constants.spacingType4
    spacingConstraint8.constant = Constants.spacingType3
}

Of course, this is only situationally correct, as our constraints don’t necessarily correlate with each other like this, but even if we have to modify multiple ViewControllers, we can use this class to our advantage (You get the idea). It’s a really powerful tool when our whole app has defining styles that are uniform everywhere. The same goes for colors, fonts, sizes and the list goes on, but we’re sticking to layout constraints for now.

Although constant-classes are handy when handling layout constraints, there a few drawbacks:

  1. The results can only be seen at runtime, which means that you need to rebuild the app, run it, then navigate to the screen (which can be pretty annoying in some situations). Yes, I know that there is such a thing as “preview” in the Assistant Editor, but it still won’t adapt to runtime values.
  2. Code. Code everywhere. And this is the main reason I’m writing this article, to reduce the lines of code that clutter our classes and make ViewControllers monstrous.

Basically, we’re left with these options: We either set up outlets for our constraints and manage them from code, while not even seeing the immediate results, or suffer through the manual adjustments.

Well, there’s another option: Programmatically changing the Storyboard.

Journey into the center of the Storyboard

So, what makes a UI descriptor file tick? Right clicking one and selecting “Open As -> Source Code” reveals its inner workings (You can switch back to the original view if you select “Interface Builder – Storyboard” here). Some are plain and simple, some are still readable while significantly larger, and some are downright disgusting (Picture over 10-20 relatively complex ViewControllers in the same file. It’s not pretty). The one thing they all have in common is that they are all XML files with the same structure. And when there’s structure, there’s also opportunity.

In this example, we have 8 UILabels, all centered horizontally, with various vertical spacings between them. This is how it looks inside the XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10116" systemVersion="15A284" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <ViewController id="BYZ-38-t0r" customClass="ViewController" customModule="DynamicConstraintScripting" customModuleProvider="target" sceneMemberID="ViewController">
                    <layoutGuides>
                        <ViewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
                        <ViewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Hi, I'm a label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CXo-pg-lcF">
                                <rect key="frame" x="254" y="40" width="92" height="18"/>
                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
                                <nil key="highlightedColor"/>
                            </label>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Hi, I'm a label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8Q8-Sg-W9g">
                                <rect key="frame" x="254" y="78" width="92" height="18"/>
                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
                                <nil key="highlightedColor"/>
                            </label>
                            <!-- Here comes another 6 labels, I'm just saving space -->
                                .....
                                .....
                        </subviews>
                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
                        <constraints>
                            <constraint firstItem="8Q8-Sg-W9g" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="9MC-TM-0v1"/>
                            <constraint firstItem="CXo-pg-lcF" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="20" id="Iv5-ZE-7uU"/>
                            <constraint firstItem="ZYs-5w-ZAL" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="NZn-Z4-c8T"/>
                            <constraint firstItem="kaU-MT-bKP" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="SUR-WC-dzt"/>
                            <constraint firstItem="kaU-MT-bKP" firstAttribute="top" secondItem="2EY-ci-7yu" secondAttribute="bottom" constant="30" id="Uxg-dq-8qV"/>
                            <constraint firstItem="aO9-n4-Dps" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="VFg-Gt-7bX"/>
                            <constraint firstItem="ZYs-5w-ZAL" firstAttribute="top" secondItem="7Fk-U4-f1N" secondAttribute="bottom" constant="13" id="Y6H-Si-l14"/>
                            <constraint firstItem="msp-ry-JHf" firstAttribute="top" secondItem="ZYs-5w-ZAL" secondAttribute="bottom" constant="15" id="YHr-fc-f39"/>
                            <constraint firstItem="8Q8-Sg-W9g" firstAttribute="top" secondItem="CXo-pg-lcF" secondAttribute="bottom" constant="20" id="d1Y-Mj-0Nt"/>
                            <constraint firstItem="7Fk-U4-f1N" firstAttribute="top" secondItem="aO9-n4-Dps" secondAttribute="bottom" constant="15" id="eV6-99-Cz0"/>
                            <constraint firstItem="2EY-ci-7yu" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="gjD-Pj-gy8"/>
                            <constraint firstItem="7Fk-U4-f1N" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="nMZ-PT-v0H"/>
                            <constraint firstItem="msp-ry-JHf" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="vn8-z1-oqJ"/>
                            <constraint firstItem="CXo-pg-lcF" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="wgE-Sv-a9m"/>
                            <constraint firstItem="aO9-n4-Dps" firstAttribute="top" secondItem="kaU-MT-bKP" secondAttribute="bottom" constant="30" id="z5Z-kB-qH5"/>
                            <constraint firstItem="2EY-ci-7yu" firstAttribute="top" secondItem="8Q8-Sg-W9g" secondAttribute="bottom" constant="30" id="zpv-jK-DbP"/>
                        </constraints>
                    </view>
                    <connections>
                        <outlet property="spacingConstraint1" destination="Iv5-ZE-7uU" id="Zq4-Zz-0DB"/>
                        <outlet property="spacingConstraint2" destination="d1Y-Mj-0Nt" id="DFf-Qo-jbj"/>
                        <outlet property="spacingConstraint3" destination="zpv-jK-DbP" id="H8F-HA-XMS"/>
                        <outlet property="spacingConstraint4" destination="Uxg-dq-8qV" id="xmm-8G-0PX"/>
                        <outlet property="spacingConstraint5" destination="z5Z-kB-qH5" id="I8P-ht-vqN"/>
                        <outlet property="spacingConstraint6" destination="eV6-99-Cz0" id="u75-FA-fzV"/>
                        <outlet property="spacingConstraint7" destination="Y6H-Si-l14" id="RbV-85-AtW"/>
                        <outlet property="spacingConstraint8" destination="YHr-fc-f39" id="LM4-5w-iJe"/>
                    </connections>
                </ViewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="308" y="439"/>
        </scene>
    </scenes>
</document>

As you can see, apart from all the gibberish, inside the ViewController’s view property, all our labels are inside the tag, and our constraints lurk below in silence. For the layout to be valid, I also added horizontal constraints, don’t mind the ones with “centerX” in them. At this moment, there’s not much we can do with them; the ordering is messed up and there are no accessible properties in the tags that we could use. If we change the constants’ value here, the interface builder will also update the constraints’ values, but that’s about it.

There are quite a few ways to exactly identify a constraint inside this XML. For starters, it has an ID that can be explicitly set in the constraint’s Attributes Inspector. Other ways are to set its tag in Attributes Inspector, and its user label in Identity Inspector. After looking through the editor, I’ve found that these properties are the best suited for identification purposes, mainly the Label, because it allows the setting of strings, not only integers (ID has to be unique, therefore it’s not adequate for “bulk” operations). Let’s change the first constraint’s Label property to “spacingType1” in the Identity Inspector, then check inside the XML to see the changes:

<constraint firstItem="CXo-pg-lcF" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="20" id="Iv5-ZE-7uU" userLabel="spacingType1"/>

You probably noticed that the label we chose is identical to the static value inside our Constants class. This is where we step through the looking glass.

Shell Script Land

For the brave and/or bold, who are still with me after that terrifying chapter title, I have some good news: I’ve already written a script that updates constraint values from a Constants file, you’ll find the link at the end.

Delete your outlet connections from the ViewController, we no longer need them (don’t forget to remove the connections in the Interface Builder as well). Since there are no more constraint outlets, remove the constant assignments too, also in the ViewController.

Now I’d like to guide you through the details on how to achieve our goals. Here’s the basic gist:

We don’t want to write constant assignments in code, so we include a script which does exactly that for us. This script is executed before compilation, so the interface of the application will be built with the values we provided.

First, add a Run Script phase to your project, you’ll find it under “Build Phases” in the project’s settings, and make sure your script phase uses the standard /bin/sh shell. After adding the phase, drag-n-drop it just above the “Compile Sources” phase by holding on to the “Run Script” title. This ensures that the constraints get updated before compilation, so every change you made will be properly reflected on your next project run. Be careful, though! Mixing up the other phases’ ordering can produce some rather unpleasant results. Here’s what your ordering should look like:

text

Whenever you build your app, this script will be executed, unless you specify otherwise. What we’re going to do is iterate through all our constant values in Constants.swift, find all the constraints in the storyboard whose userLabel property matches the variable name, then set the “constant” attribute of the XML element. Quick note: I’m not the biggest shell scripting pro, and I’m aware that some of the operations I used could be substituted or improved. Let’s start with reading the variables in Constants:

storyboard="Path/To/Your.storyboard"
constants="Path/To/Your/Constants.swift"
  
while read -r line || [[ -n "$line" ]]; do
    propName=$(echo $line | grep "static let" | sed 's/static let \([^ ]*\): CGFloat =.*$/\1/' | sed 's/\n//g')
    propValue=$(echo $line | grep "static let" | sed 's/.*CGFloat = \(.*$\)/\1/' | sed 's/\n//g')
    ...
done < $constants

We read the file line by line, filter each line with grep, then remove all unnecessary characters, and assign the results into variables. We’ll use these variables to navigate the storyboard.

if [ "x$propValue" != "x" ] && [ "x$propName" != "x" ]; then
    sed -i '' "s/^\(.*<constraint.*\)constant=\"[^\"]*\" \(.*userLabel=\"${propName}\".*\)$/\1\2/g" $storyboard
    sed -i '' "s/^\(.*<constraint.*\)\(id=\"[^\"]*\".*userLabel=\"${propName}\".*\)$/\1constant=\"${propValue}\" \2/g" $storyboard
fi

The “if” statements check whether the variables actually exist. If they are all correct, we find all the constraints that match their userLabels to the variable name. The first sed [more on sed – the editor] command removes the ‘constant=”x.y”‘ part from the XML element if it exists, and the second one “puts it back”. This is needed because if you set a constraint’s constant to zero, the “constant” attribute will be removed, and this way we ensure that it’s only included once.

The final script:

text

Now build your app! You’ll instantly see that the constraints have been updated to reflect the values in your Constants file. True, it won’t automatically readjust the frames in Interface Builder, but a script that can do that would be only a few pages longer.

Note: If you wish to “regain control” of a constraint, make sure to remove the UserLabel property, otherwise its value will always be overwritten by the script.

Endless possibilities

Well, not quite, but there are other things that can just as easily be manipulated. Fonts, colors, etc. The point of this article was to give you some new ideas on how to save your ViewControllers from unnecessary code, making them cleaner.

The first script improvement would be to specify a list of Storyboards and Constants files, I’ll get around to it soon!

There is only so much we can (or should) directly access from a Storyboard, but it’s all well. Scripting is not meant to take over the place of constraint manipulation in native code, nor is it adequate to calculate complicated formulas, as it would be a bit too complex, but feel free to experiment and contribute!

Anyway, I hope you found this article useful! Finally, here’s the link to the GitHub gist: Click Me

member photo

His Wanari career started in October 2013, he is not only an experienced iOS developer, he also has excellent English skills to explain his stuff.

Latest post by Tamás Keller

Everything You Need to Know About JSONJoy, SwiftyJSON & OCMapper