iOS Layout Adaptation

SafeArea

Since iOS 11, Apple introduced a new feature named Safe Area for UIView. In our UI designs, some UI elements are fulfill the whole screen, but some need always to display above the navigation bar, the status bar or the bottom indicator. We can easily calculate the top margin is 20px, the status bar’s height, when there aren’t any full-screen iPhones. The safe area concept is for easily adapting screen layouts for iOS developers.

There are two properties safeAreaInsets and safeAreaLayoutGuide we can used for adapting our UI elements.

We can get the safeAreaInsets’ value after the view loaded.

1
2
3
4
5
override func viewDidLayoutSubviews() {
print(view.safeAreaInsets)
}

/// UIEdgeInsets(top: 59.0, left: 0.0, bottom: 34.0, right: 0.0)
image

As you can see, the red view is the safe area. It excludes the top status bar and the bottom indicator area.
If we rotates the screen from portrait to landscape, the safe area will also change.

1
2
3
4
5
override func viewDidLayoutSubviews() {
print(view.safeAreaInsets)
}

/// UIEdgeInsets(top: 0.0, left: 59.0, bottom: 21.0, right: 59.0)
image

The safe area we talked above is about the view controller’s view. What sizes are safe ares in normal views?

We create a blue sub view in the root view controller’s root view. We just make the blue view’s edges equal to the super view.

1
2
3
4
5
6
7
8
9
10
11
12
innerContainer.backgroundColor = .blue
view.addSubview(innerContainer)
innerContainer.snp.makeConstraints {
$0.top.equalTo(0)
$0.leading.equalTo(0)
$0.trailing.equalTo(0)
$0.bottom.equalTo(0)
}
// ...
print(innerContainer.safeAreaInsets)

// UIEdgeInsets(top: 59.0, left: 0.0, bottom: 34.0, right: 0.0)

The output indicates that the blue view has the same safe area compared to its super view.
And then we reset the top constraint to be 20. The new output is:

1
// UIEdgeInsets(top: 39.0, left: 0.0, bottom: 34.0, right: 0.0)

The top size of the screen area is 59, that equals to the subview’s top safe size adds the top constraint’s size. We know that views’ safe areas are always contrasted with the device screen.

Apple provided a property additionalSafeAreaInsets to UIViewController for developers to adjust safe areas of their views arbitrarily.

The additionalSafeAreaInsets we set will be accumulated to the original safe area size.

1
2
3
4
additionalSafeAreaInsets = .init(top: 20, left: 10, bottom: 0, right: -10)

// Portrait UIEdgeInsets(top: 79.0, left: 10.0, bottom: 34.0, right: -10.0)
// Landscape UIEdgeInsets(top: 20.0, left: 69.0, bottom: 21.0, right: 49.0)
image image

Safe areas of sub-views will also be affected by the value of the property additionalSafeAreaInsets of their view controller.

LayoutMargins

In addition to SafeArea, there are another two properties layoutMargins and layoutMarginsGuide of views Apple provided for optimizing our UI layouts.

The main role of properties above is providing a standard edge margin guide when we set up edge constraints of views with their parent views.

We set constraints that constraint the edge layout of the red container to be equal to the layoutMarginsGuide of its super view.

1
2
3
4
5
6
7
8
9
10
container.snp.makeConstraints {
$0.edges.equalTo(view.layoutMarginsGuide)
}
// ..
print("\(view.safeAreaInsets)")
print("\(view.layoutMargins)")
print("\(container.layoutMargins)")
// UIEdgeInsets(top: 59.0, left: 0.0, bottom: 34.0, right: 0.0)
// UIEdgeInsets(top: 59.0, left: 20.0, bottom: 34.0, right: 20.0)
// UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
image

We can see that the red container has horizontal margins of 20 points to its super view. The 20 is the recommend horizontal margin of view controllers in the iPhone 14 Prox Max. Different devices with various screen sizes have distinct layout margins.

There are some values like top and bottom are same as the safe area insets. In practice, the layout margin calculation is related to the view’s safe area insets, but they aren’t easily accumulated.

The layout margins of root views of view controllers have only horizontal values. Maybe 18 or 20 with difference screen sizes.
A normal view’s layout margin is UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0).

The final values of layout margins of views is adding the margin with the max(safeAreaInset, 0).

LayoutMargins calculation

final_top_margin = view_top_margin + max(0, view_safe_area_top_inset)

Similar as the additionalSafeAreaInsets, UIView has a property directionalLayoutMargins for overriding the system’s default value.

1
2
3
container.directionalLayoutMargins = .init(top: 100, leading: -100, bottom: 0, trailing: 0)
print("\(container.layoutMargins)")
// UIEdgeInsets(top: 100.0, left: -100.0, bottom: 0.0, right: 0.0)

The container’s layout margins are modified directly by the property directionalLayoutMargins. Please note that the directionalLayoutMargins aren’t the view’s final margins, and we should pass it with safe area insets into our expression written above to get the final result.

Conclusion

Controls UIs should use the safe area to constraint themselves into an appropriate area. With more and more screen sizes coming, the safe area is the best way for developer to ensure that their apps can always display correct UI layouts in any devices.

The layout margin is a good tool to unify the consistency of UI views. But most applications have their own design styles and rarely can be adapted by the system of layout margins. We prefer to use constant margin configurations to resolve these margin constrains.