How to Add Dynamic Constraints to Your Swift Project

How to Add Dynamic Constraints to Your Swift Project

Over the last couple weeks I’ve been working on a MacOS prototype application using the Xcode IDE and the Swift programming language. The other day, when playing around with the layout and logic behind my controls, I realized that I needed to make one of the constraints more dynamic as the static logic on the interface builder was causing a blank space to appear whenever I hid one of my labels. I wanted the control underneath that label to be moved up so that the space would be covered up whenever I set the label to be hidden. I also wanted that same control to be moved down whenever I did need to use that label to show a message to the user. I decided to hunt down how to make this particular constraint seem more dynamic and came up with this solution. I wanted to share it with you in case you’re also looking to make your own constraint a bit more dynamic too.

Pinterest-geared image attempting to show what the post is about without any epic images. From top to bottom there's text saying "Hate the random space on your user interface when you hide a control?", an image showing a blank space between two rows of controls, the text "Why not make your constraints dynamic by adding them to your code making it easy to toggle them on and off based on the control's visibility?", an image without that space, two arrows one pointing up and one down, another image with a label displaying a message between the rows of controls, text saying "Learn more at SimplyKyra.com. Search for "How to Add Dynamic Constraints to Your Swift Project", and then two side by side images showing the interface builder's vertical space constraint GUI and a partial image of the code.

At this time I am running Xcode version 12.5 (12E262)


Over the last couple of weeks I’ve been working on a MacOS application using the Xcode IDE with Swift. I’ve more recently started overhauling the front end of my application, specifically using the interface builder on my storyboard file to move around my controls and quickly realized that I needed a more dynamic version of one of the constraints that was a little bit too restrictive for me.

As a quick aside if you want to read more about working with constraints in the interface builder you can check out Apple’s Document Archive: Auto Layout Guide.

Setup

Specifically, for this particular layout, I used controls that spanned the entire width of my application’s superview. The label causing the issues here was created to display any error message if my code wasn’t able to execute as the user might expect. Above it I had a horizontal stack view showing two checkboxes and a button while below the label I placed a scroll view, containing a text view, so the results once the button was pressed could be displayed. After putting the controls down on the main view I started from the top and worked my way down using the Pin Tool on each control in turn so they’d be positioned 6 spaces down from the one above it and their sides, both the leading and trailing edges, would be flush against either side of the superview. Finally, once I got to the bottom control, I set all four of it’s constraints so it would be pinned to the sides, top, and bottom.

Image shows the UI with three controls spanning across the window. From top to bottom you see the bottom portion of the horizontal stack view, a label centered saying "Multiline Label", and the top part of a scrollable text view.
While laying out the controls and adding the static constraints I had no problematic issues.
Image shows a screenshot of the Pin Tool with it's overlay showing how the controls each had their constraints added to nest on either side of the superview and keep a space of 6 between each control.
For each control I made sure the top field was set to 6 and the side ones were set to 0. Once the constraint is acknowledged the line leading to the field turns red. If one of the fields was already filled with the number you wanted you could simply click the line to set the constraint and turn it red instead. At the bottom the button updates with how many constraints you’re setting and, once you’re ready, you click it to finalize them. You can learn more about this in Apple’s Documentation Archive: Auto Layout Guide: Working with Constraints in Interface Builder.

Discovery

I saw no issues when setting this up; however, after setting up the logic to hide the label if there’s no error message to display, I noticed when running my application that a blank space remains where the label was whenever there’s no message. Ideally, I wanted that space to shrink so the label essentially disappears leaving no trace so I decided to hunt through the Document Outline to find the offending vertical constraint that’s set between the label and the scrollable text view and then perused through it’s attributes.

Like before the image shows the UI with three controls spanning across the window. From top to bottom, this time, you see the bottom portion of the horizontal stack view, a blank space where the label is hidden, and the top part of a scrollable text view.
When running the application I saw that the space where the label was placed was still present, and blank, when the label was set to hidden. Ideally I want that space to shrink so there was no evidence of the label having been there when I don’t need it.

While looking through the attributes for that vertical space constraint I didn’t see any obvious way to make the constraint more dynamic but I did spy a checkbox labeled Remove at build time. I quickly realized, which I had already been wondering, that the solution was going to be in the code and not in the interface builder itself. Rather than deleting the constraint right then and screwing up the layout shown in the interface builder I instead checked off Remove at build time so the interface builder would still see the constraint but that constraint won’t be in my way when working through the logic of the label within the code later.

Image shows the attributes for the vertical space constraint in question. It displays an equal relation between the top of the control below and the bottom of the control above. There's a constant space of 6 between them with a multiplier of 1. The checkbox "Remove at build time" at the bottom is checked off.
I picked out the vertical space constraint between the label and the scrollable text view from the Document Outline. Once I selected it the constraint’s attributes were then displayed in the Attribute Inspector (shown in the picture here). I quickly discovered the placeholder Remove at build time and checked it off so this constraint would only affect the interface builder and not the logic I had to put in.

I didn’t take a photo but once this constraint was removed, shown at runtime, the scrollable text view below the label disappears and I’m left with a blank space at the bottom of my application window. I wasn’t worried as I knew this would be a temporary issue.

I then hopped back online to see if I could figure out a way to programmatically create my constraints. I knew I wanted to create two individual constraints and toggle between them within my code. I wanted one that was assigned to the vertical space between the scrollable text view and the label itself (like I had in the interface builder) that would be active when the label was visible. I also knew I wanted the other one assigned to be the vertical space constraint between the scrollable text view, again, and the horizontal stack view, above the label, so when it’s activated the scrollable text view would be moved up covering the hidden label.

Luckily, as I knew what I wanted, I was able to quickly discover my answer in the Apple’s Documentation Archive: Auto Layout Guide under the Programmatically Creating Constraints page. On it they had an amazing diagram that showed two items close together, a labelled linear equation, an example line of code, and then they looped the previous pieces together in a grid so you could easily see how it all works together. I took a screenshot, below, so you’d have it readily available. While looking at the image I realized I should figure out the linear equation that I wanted to set and quickly jotted it down.

Screenshot of a section of text showing an image, a linear equation, a line of code, a grid, and text explaining each part. The link to the website is in the caption below the image.
This screenshot was taken on May 27th, 2021 from Apple’s Documentation Archive: Advanced Auto Layout: Programmatically Creating Constraints page.
Image showing a quick diagram with three rectangles stacked on top of each other to show the controls (from top to bottom): stack view, label, and scrollable text view. On the left and right sits the linear equations showing the constraint I'd need if the label was visible (between label and scrollable text view) or invisible (between stack view and text view).
While referring to the above I quickly jotted down scrollableTextView.top = 1 * label.bottom + 6.0 but figured this image would better describe my logic.

Further down on the same page they showed how to directly instantiate constraints using the NSLayoutConstraint class and I knew that that’s what I needed to do. Rather than use their example where they instantiated and activated the constraint all at once I instead broke it up a bit. This was the beginning of my solution.

Screenshot from the Advanced Auto Layout page showing how three constraints where created and activated in the code. If you click the link in the caption you can find it under "NSLayoutConstraint Class"
This screenshot was taken on May 27th, 2021 from Apple’s Documentation Archive: Advanced Auto Layout: Programmatically Creating Constraints page under the caption NSLayoutConstraint Class.

My Solution

To create the constraints programmatically I knew I needed to access all three of the controls in question within my back end code. Specifically, I needed to connect the stack view, the label, and the scrollable view containing my text view to my code via an outlet.

Image is the same sketch of the three controls, as rectangles, as before. This time the left and right sides show the names of the two controls you'll need access to to create the constraint when the label is visible or hidden.
I figured I’d borrow the earlier diagram and use it to show which two controls I needed to access for either of the constraints. Since both cases need the scrollable text view I only need a total of three controls to be hooked up before making them.

To do this I made sure my storyboard file, showing the interface builder, is open. I next want the back end code, my view controller, open at the same time so I’ll need a split screen. You could click the icon at the top right to create a split screen and then make sure the proper files are open but I instead prefer to hold down the Option key and click my view controller in the Document Viewer to set it up. This process automatically creates a split screen with the previously opened file, in this case the storyboard, on the left and the newly opened file on the right. After making sure that the controls I want access to are centered on the left I get to work moving them into my code. I do this by first holding down the Control key, clicking on the control I want, and then, without releasing the mouse button, dragging the line that pops up to the view controller code. As you hold the end of the arrow above the code a caption will pop up saying Insert Outlet. Once you let go a popup appears allowing you to configure the properties before creating the connection (called an outlet). At the top of this popup will be a dropdown showing the type you can insert. Make sure this says Outlet rather than Action. You’ll be required to type in the name of your new outlet before clicking on Connect. I then repeated this process for the other two controls. If you want more information about Connecting Objects to Code you can check out Apple’s Documentation Archive: Xcode Overview: Building an User Interface.

Image shows a screenshot of my code with the arrow coming from the label and ending in my code under other outlets.
After opening up both the interface builder and the view controller code I then held down the Control key, clicked and dragged on the control I wanted to connect, and brought the end of the arrow to my code so I could insert an outlet.
Image shows the same code as before but this time there's no line and a popup is coming out of the space I dragged the arrow to. Here I can set the connection, see the object, create a name, and select a type before clicking either the "Cancel" or "Connect" button.
After releasing the mouse button a popup appears letting me confirm I want to connect an outlet (after taking the photo I changed the dropdown from action to outlet), lets me name it, set it’s type, and then I finally pressed Connect to add it.

Once the three controls were connected to my code I quickly declared my two constraints right below them so they’d be able to be used in several locations within my class. I had planned to leave them uninstantiated but received an error as it

Expected member name or constructor call after type name

I couldn’t instantiate the constraint then and there with the outlets as they

Cannot use instance member ‘outlet(s)‘ within property initializer; property initializers run before ‘self’ is available

With both paths closed, for justifiable reasons, I decided to instantiate them without any arguments so they would each be a blank NSLayoutConstraint for now.

Screenshot of my code showing five lines. The first three are the connected controls for the label, stack view, and scroll view (containing the text view). Below it are the two private constraints.
After connecting my outlets from the interface builder, as shown by the filled in bullet point on the corresponding line numbers, I quickly declared and instantiated the two NSLayoutContraints I planned to use in the viewDidLoad function and elsewhere.
// The three controls I'll need access to to create the constraints
// The stack view above the label
@IBOutlet var stackViewJSON: NSStackView!
// The label in question
@IBOutlet var lblLoadJSONmessage: NSTextField!
// The scroll view holding the text view; located under the label
@IBOutlet var textViewChangingJSON: NSScrollView! 
// The two private constraints; temporarily instantiated to empty constraints
private var constraintScrollViewToStackView = NSLayoutConstraint()
private var constraintScrollViewToLbl = NSLayoutConstraint()

Once that was done I went up to the viewDidLoad function to instantiate the constraints properly. Since this method is always called once the view is loaded I knew instantiating the constraints here would allow me to use the proper outlets and since this function is executed before anything the user could interact with I also know the constraints would be set before I could possibly toggle them on or off in the code. I overwrote both my private constraints, as they had both been set to blank values, with the new version using the proper outlets and then, since the label is hidden on load, activated the one constraint that covers up the label so the user wouldn’t be able to see where the label was once the application loads. Since the original constraint in my interface builder is removed at build time this new constraint won’t interfere with it.

Screenshot of my code, within the viewDidLoad function, showing how I created my two constraints and activate the one I wanted on by default.
In the viewDidLoad function I overwrote each of the two constraints with a new one using the outlets matched to my controls. I then activated the one constraint that hides the label from the user.
override func viewDidLoad() {
    // Allows the super class to handle it's code first  
    super.viewDidLoad()
     
    // Set up the two constraints I want to toggle between
     
    // This constraint sets the distance between the stack view and the scrollable text view thus hiding the label as the scroll view covers it up. Since there's a static constraint between the label and the stack view above it this essentially places the scrollable text view at the same place as the currently hidden label.
    constraintScrollViewToStackView = NSLayoutConstraint(item: textViewChangingJSON!, attribute: .top, relatedBy: .equal, toItem: stackViewJSON, attribute: .bottom, multiplier: 1.0, constant: 6.0)
    // This constraint makes space for the label as it positions the scrollable text view under the label instead of over it like the previous constraint. 
    constraintScrollViewToLbl = NSLayoutConstraint(item: textViewChangingJSON!, attribute: .top, relatedBy: .equal, toItem: lblLoadJSONmessage, attribute: .bottom, multiplier: 1.0, constant: 6.0)
    // Both are inactive by default so I next turn on the one I want active on load
    constraintScrollViewToStackView.isActive = true      
}

Two quick asides:

I call super.viewDidLoad() at the beginning of my viewDidLoad function so the superclass can handle it’s setup before I add my own. If interested you can read more about Why/When Do We Have To Call Super.ViewDidLoad? through izzi{Swift}.

While writing this post up I wondered it I could just add an initializer to my class and use that to initialize my already declared private constraints. I got the code compiling but when I ran my application it crashed

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

because my outlets were still set to nil as my view hadn’t loaded yet. I fixed it by returning my code back to how it was before as shown in this post. Specifically the blank initialization to the private constraint declarations and the proper initialization back to the viewDidLoad function.

Anyway once the two constraints were declared and properly instantiated it’s time to use them. To do this I moved down in my code to the function where I set my label to visible or hidden based on whether there’s an error message or not. If there’s an error message then the label needs to be visible .isHidden = false, the constraint that hides the label needs to be inactivated isActive = false, and the constraint that shows the label needs to be active isActive = true. If there isn’t an error message then the label needs to be invisible .isHidden = true, the string is set to blank .stringValue = "", the constraint that shows the label needs to be inactivated isActive = false, and the constraint that hides it needs to be activated isActive = true. Since you can’t have two competing constraints you’ll need to inactivate the currently active one before activating the other one. I went the opposite way in the else if section and got an error so I swapped the two lines.

Image shows a section of my code where I set the visibility (isHidden) of my label and activate (isActive) my constraints based on whether there's an error message or not.
This shows my code segment where I deactivate the active constraint .isActive = false before activating the other constraint .isActive = true. This is the proper ordering.
Image shows almost the same code as the previous picture. In this case, though, the last two lines in the "else if" section are reversed so my application encounter an error when I tried to activate the one as the other already was active and they conflict.
The first time through I had copied my code from the first section and just changed the boolean values so I ended up with an error when I tried to activate the conflicting constraint. After swapping the two lines it worked great.
// When the label has a value and is set visible I deactivate the constraint that hides the label and then activate the constraint that shows it.
self.label.stringValue = message!
self.label.isHidden = false
constraintScrollViewToStackView.isActive = false
constraintScrollViewToLbl.isActive = true
 
// When the label doesn't have a value, as there's no error message, I set it to invisible, blank the string value, deactivate the constraint showing the label, and activate the constraint hiding it.
self.lblLoadJSONmessage.isHidden = true
self.lblLoadJSONmessage.stringValue = ""
constraintScrollViewToLbl.isActive = false
constraintScrollViewToStackView.isActive = true

With the constraints declared, instantiated, and activated (or inactivated) it’s time to test this out!

Image shows the application with no space between the stack view and the text view.
By default, on load, I activated the constraint that sets the distance between the stack view (where the two checkboxes and the button is) and the scrollable text view (the white square below). This means the label appears to have never been there. Looking great so far!
Image shows the application with a space between the stack view and the text view so you can read the error message set on the label.
After running the code that results in an error message I was happy to see that the scrollable text view was moved down and the label was set and visible. After this screenshot was taken I ran the code with no error message which resulted in the scrollable text view moving up and the label once again disappearing just like the previous image. So overall it works great!

And with that the constraint went from a static immovable force on the interface builder to a dynamic one that fits my needs through the back end code. I was vaguely surprised at how amazing it worked and love how simple, in the end, it was to create. I wanted to share what I did with you in case you’re looking at creating something similar yourself.

If you do use this on your code I’d love to hear from you in the comments below to find out how it worked and/or how you used it. I hope your coding is going well and your day is great.


If you’re interested in getting any of my future blog updates I currently come out with a new one every Wednesday and share them to my Facebook page and Instagram account. You’re also more than welcome to join my email list located right under the search bar or underneath this post.



Browse Related Topic(s)

Technology Apple MacOS Swift Xcode

Related Posts

Latest Posts