Orientation Responsive UI in iOS

Prakhar Tripathi
Codewords
Published in
8 min readApr 16, 2020

Have you ever encountered a situation where the landscape UI does not quite match the portrait UI - meaning the constraints that were used to set up the UI in the portrait mode no longer match the constraints that are supposed to set up the UI for the landscape mode? Quite recently I found myself baffled in one such scenario - I had no idea where to begin. Upon a little research on our favourite friend Google, I found out one particular way of doing this - using 2 sets of constraints, one for landscape and one for the portrait.

You could create one set of constraints in the storyboard, for example, the portrait mode (since that is the default orientation of the storyboards) and the other set for landscape programmatically. OR you can create both sets programmatically - I leave that up to you, whatever you are more comfortable with. For solving my problem I went ahead with the former since I am a big fan of storyboards.

For our example, let us consider a simple screen with the following UI in the portrait and the landscape view -

Our sample screen in different orientations

It’s a very simple application with the following components -

  1. 2 UIViews (view1, view2) with a label inside each - the labels are arranged to be always in the centre of its view
  2. 1 view (view3) at the bottom with 3 buttons in it - the buttons again have a top, right, bottom, and left constraint from everything and are always of equal width to each other - all achieved via storyboard constraints.

Pressing the buttons just apply different shades of that color to the 2 views, no rocket science.

Now lets come to the interesting part where we change constraints depending on the orientation of the device. The trick is simple, whenever you detect a change in the orientation, find the new orientation, break the constraints of the previous orientation and apply the constraints of the new orientation.

First things first - Breaking Constraints

When I say breaking constraints, its usually a 2 step process -

1. You need to know which constraints to break - In my example I know I have to break the constraints that position the 3 views one under each other and the ones that give each other a particular height. If I see my document outline of my view controller I find out that I don’t care about the constraints that are inside my views as I want to preserve them. So that leaves me with only the constraints that are part of my View Controller’s view -

The constraints under the red box, that’s what I’d like to break.

2. Once you find out which constraints to break, you need to find a way to break them in one go, which is not too complex and easy to the eye also - IBOutlets to the rescue! Remember how you can control and drag on an element on the storyboard to create an IBOutlet on the View Controller? You can do the same for constraints. In my storyboard, I create an outlet as given

@IBOutlet var IBConstraints: [NSLayoutConstraint]!

Now all I have to do is to control drag on all my constraints in the red box in the image above and assign them to this array of NSLayoutConstraint and voila - I have a reference of all my storyboard constraints that I want to break. Breaking them is as simple as -

NSLayoutConstraint.deactivate(IBConstraints)

Once you have deactivated one set of constraints, in our case portrait(or Interface builder), you need to immediately activate the next set of constraints. To activate the next set of constraints you need to create one.

Creating constraints programmatically for landscape

Let’s go back to our landscape UI requirement and try to figure out for a moment how we want to achieve that.

There are 2 ways to do this as far as I can think -

  1. We can begin up the top views first and try to figure out where to start the first view and where to end the second view and then match the bottom view, of course taking into consideration the safe areas onto the newer iPhones.
  2. We can calculate the start and the end position of the bottom view and then align the leading constraint of view1 to the leading constraint of view3 and align the trailing constraint of the view2 with the trailing constraint of view 3.

I decided to go for the second one cause of simplicity reasons. You might choose to go with one or some other way dependent on your design and that’s fine also. Just remember - you need proper anchor points around which you can layout the rest of your UI.

Now we are good to create constraints programmatically for the landscape UI. Or are we?

Remember that we initially set up constraints for view1, view2 and view3 from the Interface Builder. Now we have broken them and want to create constraints programmatically for them. When we do this, we cannot understate the importance of -

view1.translatesAutoresizingMaskIntoConstraints = false

view2.translatesAutoresizingMaskIntoConstraints = false

view3.translatesAutoresizingMaskIntoConstraints = false

translatesAutoresizingMaskIntoConstraints” basically tells the system, “Hey, please don’t automatically create constraints for this view based on whatever information you have about them and trust to use the ones that I am going to provide.” If you don’t do this, then you are risking constraints conflicting somewhere. But now that we know about this we are ready to proceed.

Let’s start with applying constraints the view3 first because we are going to anchor the other 2 views with this. We need to create 4 constraints for this view - where do we want the view to be placed on this Y-axis(anchor it from the bottom), where should it start from the left, where should it end on the right, and what height it should take. We will use the commonly used class to create this constraint -

NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:)

You should play with the above class for creating constraints a bit so it does not feel alien to you, but I’ll very quickly break down the arguments for you —

  1. item - The view to which you want to apply this constraint to.
  2. attribute - What attribute of the item do you want to apply the given constraint to, can take values like .left, .right, .top etc
  3. relatedBy - The mathematical relation that you will use to relate it to a second object, like .equal, .greaterThan, etc
  4. toItem - The item to which you wish to apply a constraint to the given view
  5. attribute - Again, what attribute of this item do you want to use to position the first item, can take values like .left, .right etc.
  6. multiplier - With what multiplier do you want to relate the two quantities. For example, if you want the two attributes to be the same then the multiplier will be 1.0, if you want the two attributes to be related by half then the multiplier will be 0.5.
  7. constant - Any constant value that you want to add to the given constraint.

So armed with that information, let’s try to give the view3 a bottom constraint-

let view3PinToBottom = NSLayoutConstraint(item: view3, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: -SafeAreaValues.getBottomConstant())

The above piece of code says that I want my view3’s bottom to be equal(multiplier is 1.0) to the main view’s bottom minus any value of the bottom safe area, if available. You can check out the code for `SafeAreaValues.getBottomConstant()` but its something that we are not concerned as a part of this problem, but all it does is to account for the bottom safe area value in newer iPhones.

PS - view here is the main view managed by our View Controller.

Activating this constraint is as easy as -

view.addConstraint(view3PinToBottom)

Now that we have activated this constraint, there is one more important thing that we need to do here. What if the user wants to go back to the portrait mode? Sounds easy right, you disable the created constraint and activate the constraint in our outlet @IBOutlet var IBConstraints: [NSLayoutConstraint]!

Yes, its that straight forward, only that you need to remember which constraint have you created to break it in the first place. So to do that, once we activate the created constraint we put them in an array of NSLayoutConstraint so that we can disable the constraints in that array. It’s pretty straightforward to put it into an array -

landscapeConstraints.append(view3PinToBottom)

Creating the first constraint will take the most time. But once you get the hang of what you are doing, the rest will be super easy and fun.

You can find the rest of the constraints required to get our landscape view in the file ConstraintsHelper.swift in the repository given at the end of this article.

One final step would be to detect orientation changes - when to know which constraints to apply. The amazing APIs by Apple makes it very easy to do so. You need to set up a notification observer in your viewDidLoad method that listens to didChangeStatusBarOrientationNotification and provides it with a selector that can be fired when the orientation changes. For example -

NotificationCenter.default.addObserver(self, selector: #selector(self.orientationChanged), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil)

Inside your orientationChanged selector, you can find the device orientation by getting the value of statusBarOrientation like -

let deviceOrientation = UIApplication.shared.statusBarOrientation

An example of what this method might look like is as follows -

You can find the completed version of this sample project at https://github.com/prakhar30/OrientationResponsiveUI

You can use this method to easily switch constraints based on your device’s orientation or any other use case that you have. To summarize our method -

  1. Create portrait constraints in the interface builder.
  2. Identify all the constraints that you need to break to change the layout and connect them to an IBOutlet array of the type NSLayoutConstraint.
  3. Create another set of constraints that convert your view to the required landscape view and store them in another NSLayoutConstraint array.
  4. Determine the orientation changes by using orientation change notification.
  5. According to the determined orientation, break the constraints of the other orientation and activate the constraints of the target orientation.

Thank you for making it through such a long article, but I hope a very helpful one. I’ll be more than interested to know if you have solved this kind of problem using some other technique. Would also absolutely love to hear from you for any feedback, suggestions or other clarifications that you need. You can get in touch with me using the comments section or find me at www.linkedin.com/in/prakhartripathi30

Leave a clap(or 2,3,5) if you liked the article. Cheers!

--

--