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:
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!
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)
}
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:
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.
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:
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
notlayoutMargins
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
tofalse
on the view controller.