Changing Root View Layout Margins

When Apple introduced layout margins in iOS 8 they made the decision to have the system manage the margins of a view controller’s root view. This allowed them to vary the left/right margins depending on the size class (16 points for compact width and 20 points for regular). The downside was that you could not alter these margins. That changed in iOS 11.

Layout Margins - A recap

The layoutMargins property of a UIView is of type UIEdgeInsets and defines the top, left, bottom and right insets that when applied to the view’s frame define the view’s margin. For example, to set left and right margins of 64 points for a view:

myView.layoutMargins = UIEdgeInsets(top: 0, left: 64, bottom: 0, right: 64)

The view property of a UIViewController references the root view managed by the view controller. Unfortunately before iOS 11 you could not change the layout margins of this view. The system enforced left and right margins of 16 or 20 points depending on the size class.

Suppose I have code in my view controller to create a label and constrain it to the leading and trailing anchors of the root view margins:

let label = UILabel()
label.text = "Hello World"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)

let margins = view.layoutMarginsGuide
NSLayoutConstraint.activate([
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    label.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
    label.trailingAnchor.constraint(equalTo: margins.trailingAnchor),
    ])

By default, on an iPhone device in portrait the label ends up 16 points from the leading edge of the screen:

System margin

Changes in iOS 11

To change the root view layout margins to have 64 points on the left and zero on the right we can write:

view.layoutMargins = UIEdgeInsets(top: 0, left: 64, bottom: 0, right: 0)

For iOS 10 and earlier this had no effect but for iOS 11 it now works!

iOS 11 root margin of 64 points

Note there does not yet seem to be a way to change the layout margin of the root view in Interface Builder (Xcode 9).

System Minimum Layout Margins

There is one more complication if you want a margin less than the system minimum of 16 or 20 points. In iOS 11 Apple introduced the viewRespectsSystemMinimumLayoutMargins property on UIViewController. This is a Bool which is true by default to match the behaviour of iOS 10. The system manages the layout margins of the root view and enforces a minimum margin of 16 or 20 points based on the size class.

If you want a margin less than the minimum set viewRespectsSystemMinimumLayoutMargins to false in your view controller. For example, to have an 8 point margin on the left and zero points on the right:

if #available(iOS 11, *) {
 viewRespectsSystemMinimumLayoutMargins = false
 view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0)
}

iOS 11 8 point root margin

Directional Layout Margins

One other change that Apple made in iOS 11 is to introduce directional layout margins that are aware of Right-To-Left (RTL) languages. This follows the pattern used when creating constraints with layout anchors. See this earlier post about RTL Languages.

If we go back to our example of a left margin of 64 points and a right margin of zero points:

view.layoutMargins = UIEdgeInsets(top: 0, left: 64, bottom: 0, right: 0)

We can test this layout with RTL languages by changing the options for the run scheme (⌘>) to use a Right to Left Pseudolanguage:

Run Scheme

Unfortunately the layout margin is not aware of RTL languages so our label ends up positioned at the zero point right margin when we want it to be inset by 64 points.

right margin

To fix this iOS 11 adds the directionalLayoutMargins property to a UIView to be a RTL language aware replacement to layoutMargins.

This is an NSDirectionalEdgeInsets struct which differs from a UIEdgeInsets struct in having leading and trailing insets instead of left and right insets. The system applies the margins based on the layout direction. So the leading margin is on the left for left-to-right languages and on the right for right-to-left margins.

So instead of setting a 64 point left margin we set a leading margin:

if #available(iOS 11, *) {
  viewRespectsSystemMinimumLayoutMargins = false
  view.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 64, bottom: 0, trailing: 0)
}

Our label is now inset at 64 points for a RTL language:

RTL Margin

The system keeps the layoutMargins property of the root view in sync with the directional layout margins. So the left inset will take the value of the leading or trailing margin depending on the layout direction.

Summary

This is all getting confusing so here is the summary:

  • To support RTL languages always create constraints to the leading and trailing anchors not the left and right anchors.
  • For iOS 11 and later use directionalLayoutMargins not layoutMargins when changing a margin.
  • If you want the root view of a view controller to have a margin smaller than the system minimum remember to set viewRespectsSystemMinimumLayoutMargins to false on the view controller.