Adaptive layout for iOS in Swift

How to adapt views and constraints without size classes

How to adapt a view dynamically in Interface Builder? Using the aspect ratio technique.

The idea is to constraint the view to its own aspect ratio and add the additional constraint of equal width or height of superview.

The choice of equal width or height depends on the plane based on which you want to resize the view proportionally.

This technique is illustrated in this GIF:

aspect ratio technique

The view is adapted! But what about the constraints? We don’t have an easy way to adapt the constraints based on device screen size.

In this article, I’ll explain how we could address this issue both in the storyboard and programmatically through the use of a simple helper and custom class.

Let’s dive in!

Device enum

We have to define Device enum and conform it to Raw Representable.
Device enum will help us keep the device screen dimensions in one place.

First, we have to specify Device enum cases. Based on the app you’re building, the enum cases will vary. In the example below, I’ve added only iPhone devices since I don’t intend to support the iPad.

Second, to set case raw value as CGSize, we have to conform enum to Raw Representable.

To conform to raw representable we have to add a typealias for raw value, in our case we have to specify CGSize:

Third, we have to implement init with our raw value type:

And lastly, add a rawValue computed property:

Device.swift (line 21–55)

That’s it! All that’s left is to specify the concrete device model based on which the app design was made:

Device.swift (line 11–19)

Adaptive Layout Helper

This helper contains resized and adapted functions as well as dimension computed property.

Let’s review them in order.

Adapted function

AdaptiveLayoutHelper.swift (line 37 -54)

In order to get the current device screen dimensions, we have to call UIScreen.main.bounds.size:

To adapt CGFloat in base dimension (design dimension) passed to the function, first we need to calculate the ratio of base dimension size to base screen size:

Then we have to multiply the current screen width or height by the ratio to get the adapted CGFloat for the current device screen size:

Resized function

AdaptiveLayoutHelper.swift (line 15–35)

The main purpose of the resized function is to resize passed CGSize preserving the initial aspect ratio. We can choose which dimension will be taken into account when resizing the base CGSize: width or height.

There are three steps to resize the base CGSize to the current device screen size:

First, we calculate the aspect ratio of the original CGSize:

Second, we need to calculate the new dimension size (width or height), which was chosen as the base dimension that will be used for resizing:

Third, we have to multiply the new dimension size (width or height) by the aspect ratio to get the other dimension size resized according to the initial aspect ratio:

Dimension computed property

AdaptiveLayoutHelper.swift (line 11–13)

This computed property will help us dynamically change the dimension based on the device orientation. The use of this property is only justified if the app supports both orientations (landscape and portrait).

Adapted constraint class

Adapted constraint is a subclass of NSLayoutConstraint. The main task of this class is to adapt the constraint’s constant. Let’s take a closer look at it.

It has initialConstant which holds an optional initial constant of the constraint. We need this property in order to reset the constraint’s constant after orientation change.

Then the class overrides awakeFromNib function in which we pass two functions saveConstant and adaptConstant:

AdaptedConstraint.swift (line 11–22)

Let’s review the functions.

AdaptConstant function

AdaptedConstraint.swift (line 26–30)

In adaptConstant function, we adapt constant by using the adapted function from Adaptive Layout Helper.

To get the proper dimension for the adapted function, we use another function called getDimension.

GetDimension function

AdaptedConstraint.swift (line 32–45)

To get the dimension for constraint we pass NSLayoutConstraint.Attribute to getDimension function. Inside the function body, we switch attributes.

In the first case, we enumerate all NSLayoutConstraint.Attribute which represent the width dimension.

In the second case, we enumerate all attributes which represent the height dimension.

If constraint’s attribute doesn’t match any case, we’ll return nil.

SaveConstant function

AdaptedConstraint.swift (line 50–52)

The function is pretty simple, it assigns a constraint’s constant to initialConstant variable allowing us to reset the constant later on.

ResetConstant function

AdaptedConstraint.swift (line 54–58)

The function assigns initialConstant value to self.constant thus resetting the constraint’s constant.

Adaptive font

To adapt font size, we need to extend CGFloat with adaptedFontSize computed property:

CGFloatExtension.swift (line 11–15)

For convenience, let’s create a font enum which will contain static functions of font typefaces:

Font.swift (line 11–19)

In the UIFont initializer, as a size parameter, we will pass a font size modified by adaptedFontSize computed property. Now when we call any of our static functions it’ll return a font size adapted to the current screen dimension.

Below you can see the usage example:

ViewController.swift (line 55)

Usage

Storyboard

To adapt constraints in Interface Builder, we now can use AdaptedConstraint class.

In the example below, I’ll add the top and bottom constraints to my UIView and specify AdaptedConstraint class in Identity Inspector.

That’s it! Now the constraints will be adapted to all screen sizes.

Programmatic

We will create a button programmatically and constraint it using NSLayoutConstraint with adapted constants.

First, we should initialize our constraints:

ViewController.swift (line 33–36)

To resize the button proportionally, we’ll use the resized method to get adapted CGSize:

ViewController.swift (line 77–79)

The result will be used as a constant for height and width constraints:

ViewController.swift (line 89–101)

All that’s left is to activate constraints and call the setup function inside viewDidLoad:

Orientations support

If you need to support both portrait and landscape orientations, add a UIView extension called updateAdaptedConstraints:

UIViewExtension.swift (line 11–22)

This extension allows us to get all AdaptedConstraints of the UIView, reset their constants, and adapt constants to the new screen dimension. These actions are necessary in order to adapt constraints when the orientation has been changed.

ViewController.swift (line 103–108)

The updateButtonConstraints function updates button constraint constants when orientation changes.

updateButtonConstraints function alongside with updateAdaptedConstraints function are wrapped inside updateConstraints function:

ViewController.swift (line 72–75)

Which is then called inside viewWillLayoutSubviews in order to respond to device rotation:

ViewController.swift (line 26–30)

Result

Here you can see the screenshots from different iPhones that have different screen dimensions:

Source code

You can get tutorial source code and have a look at usage example in my GitHub repository: https://github.com/creimbord/adaptive-layout-uikit/tree/master/AdaptiveLayoutUIKit

iOS Developer. Work and enjoy life in Moscow.