본문 바로가기

학습 노트/iOS (2021)

159. Layout Margin & Layout Guide

Layout Margin

Layout Margin은 View의 Bound와 View의 내용 사이의 여백으로 iOS 8과 함께 추가된 속성이다.
left, right, top, bottom 네 방향을 개별로 지정한다.
Layout Margin은 iOS 11 이전과 이후가 다른 방식으로 동작하고, iOS 11 이후로는 Safe Area나 다른 속성에 영향을 받을 수 있다.

버전에 따른 차이

각 View들은 상위 View의 Margin을 기준으로 정렬되어있다.

사진은 강의의 iOS 10 시뮬레이터의 모습니다.
붉은색 View의 제약이 Root View의 Margin을 기준으로 하기 때문에 Navigation Bar와 Tap Bar에 가려진다.

View Controller의 Root View인 경우와 아닌 경우의 결과가 달라진다.
View Controller의 Root View인 경우 top, bottom Margin이 모두 0이고, left, right가 20pt이다.
left, right는 Size Class에 따라 16pt가 되는 경우도 존재한다.
반면 붉은색 View의 기본 마진은 8pt이다.

Root View의 Size Ispector에서 Layout Margin을 Default에서 Fixed로 변경한다.

이후 모든 값들을 50pt로 변경하면 주황색 View가 축소된다.

마찬가지로 주황색 View의 Margin도 50pt로 변경하면 보라색 View의 크기가 새 Margin에 맞춰 축소된다.

하지만 iOS 10의 시뮬레이터에는 변경사항이 적용되지 않는다.
Design Time의 결과는 최신 iOS 버전에서의 결과물을 표시한다.
즉, iOS 11 이전의 버전에서는 Root View의 Margin을 변경할 수 없기 때문에 Interface Builder와는 다른 결과를 보여준다.
하지만 Root View가 아니라면 변경된 Margin을 사용할 수 있다는 이야기 이기도 하다.
실제로 붉은색 View의 Margin은 변하지 않았지만 파란색 View의 Margin은 상하좌우 50pt씩 적용된 상태로 로그에 출력된다.

반면 최신의 iOS에서는 Design Time과 같은 결과를 보여준다.

Layout Margin

변경했던 Margin을 원래대로 복귀한 뒤 실행한다.

iOS 14의 아이폰 12 미니에서의 top, bottom margin은 94, 83이고, left, right는 16이다.
이는 iOS 11부터 Root View의 Margin이 Safe Area를 고려하기 때문이다.
즉 94pt는 Navigation Bar와 겹치지 않는 최소 Margin이고, 83pt는 Tap Bar와 겹치지 않는 최소 margin이다.
주황색 View는 Root View가 아니기 때문에 8pt의 기본 Margin을 가지고 있고, Safe Area를 고려하지 않는다.

Layout Margins 항목 아래에 Safe Area Relative Margins가 존재한다.
해당 옵션이 Safe Area를 고려하는지를 결정하는 옵션이다.

또한 이전과는 달리 최신 버전의 시뮬레이터는 Root View의 Margin을 변경해도 이를 적절히 반영한다.

더불어 Layout Margin 속성 대신, Directional Layout Margin 속성을 사용한다.
이는 언어의 방향을 고려하기 위해 위와 같이 left, right 대신 leading, trailing을 사용한다는 의미이다.
또한 속성의 형식도 UIEdge Inset이 아닌 NSDirectional Edge Inset을 사용한다.

즉 iOS 11을 기준으로

  • Root View의 Margin이 Safe Area를 고려한다.
  • Root View의 Margin을 변경할 수 있다.
  • Directional Layout Margin을 사용한다.

특히 Directional Layout Margin은 iOS 10 이하의 버전과 호환되지 않으므로,
해당 버전들을 타겟으로 한다면 사용에 주의가 필요하다.
즉 이 경우엔 코드를 통한 예외처리가 필요하다.

func setupMargin() {
	if #available(iOS 11.0, *) {
		OrangeView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 30, leading: 30, bottom: 30, trailing: 30)
	} else {
		OrangeView.layoutMargins = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30)
	}
}

위와 같이 처리하면 된다.

마진 기준 제약 (Constraint to Margin)

Layout Margin은 보통 Interface Builder에서 표시가 되지 않아 소홀히 하는 경향이 있지만,
View의 Margin에 따라 배치하는 것과 Bound를 기준으로 배치하는 것에는 큰 차이가 있다.
또한 Margin은 Safe Area와 SuperView의 Margin의 영향을 받을 수 있다.

추가되어있는 두개의 View 중 주황색 View는 Margin을 따라, 보라색 View는 Bound를 따라 배치해 본다.

제약 메뉴를 통해 제약을 추가한다.
이때 Safe Area가 아닌 View를 기준으로 제약을 추가해야 하고,
Constrain to margins를 활성화해야 Margin을 기준으로 제약이 추가된다.
top, leading, bottom을 0으로 설정하고, 너비만 128pt로 제약을 추가한다.

보라색 View도 제약을 추가한다.
마찬가지로 View를 기준으로 top, trailing, bottom을 0으로, 너비를 128pt로 하되,
Constrain to margins를 비황 성화해 Bound를 기준으로 추가한다.

보라색 View가 Navigation Bar와 Tap Bar를 가로지른다.
당장 보더라도 차이가 확연하다.

이번에는 Root View의 Margin을 50으로 변경해 보았다.
주황색 View의 크기가 줄어든 것에 비해 보라색 View는 영향을 받지 않는 것을 확인할 수 있다.

Safe Area Relative Margins를 비활성화하면 자동으로 고려하던 Sage Area를 무시한 기본 margin을 확인할 수 있다.
Margin의 사용 이유가 Cliping을 방지하고 가독성을 향상하기 위함이기 때문에 해당 기능을 사용하는 것이 좋다.

마진 최솟값 (Minimum Layout Margin)

iOS 11부터 Root View의 Margin을 변경할 때는 주의할 점이 있다.

Minimum Layout Margin Scene에 추가된 View는 Root View의 Margin을 고려한 모든 공간을 채우도록 되어있다.

이때 Root View의 Margin을 위와 같이 변경해보면 top, bottom은 5pt로 변경됐지만,
leading, trailing은 5pt가 넘는다.
해당 Margin의 크기는 20pt 혹은 16pt인데, 이것이 Root View의 Minimum Margin에 해당한다.
즉 이 값보다 작은 Margin이 사용되면 무시되고 Minimum Margin이 사용된다.
이는 Root View에 표시되는 내용이 적절히 표시되도록 보장하기 위함이다.
만약 그럼에도 불구하고 더 작은 Margin을 사용해야 한다면 Controller가 Minimum Margin을 사용하지 않도록 변경해야 한다.

Minimum Layout Margin 해제하기

//
//  MinimumLayoutMarginViewController.swift
//  Margin and Guide Practice
//
//  Created by Martin.Q on 2021/12/14.
//

import UIKit

class MinimumLayoutMarginViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
		
		viewRespectsSystemMinimumLayoutMargins = false
    }
    
	override func viewDidLayoutSubviews() {
		super.viewDidLayoutSubviews()
		
		if #available(iOS 11.0, *) {
			dump(systemMinimumLayoutMargins)
		} else {
			
		}
		dump(view.layoutMargins)
	}

}

위와 같이 viewRespectsSystemMinimumLayoutMargins 속성을 false로 변경한다.

그럼 이전과 달리 Minimum Margin인 20pt이나 16pt이 아닌 5pt가 적용된 것을 볼 수 있다.

슈퍼뷰 마진의 영향 (Preserve Superview Margin)

Preserve Superview Margin Scene에 Root View의 Margin을 보면 leading과 Trailing이 100으로 설정되어있다.
화면의 붉은 선은 Leading Margin의 위치에 맞게 배치되어있다.
주황색 View와 보라색 View는 Root View의 Bound를 기준으로,
각각의 View의 하얀색 View는 기본 Margin에 맞게 제약이 추가되어있다.

각각의 View들은 Super View에 영향을 받지 않는 각각의 Margin을 사용한다.
Root View의 leading과 trailing이 100 임에도 하얀색 View가 이를 초과해 표시되는 것은 이런 이유이다.

보라색 View의 Size Inspector에서 Preserve Superview Margins를 활성화하면
Superview의 Margin에 맞게 축소된다.
여기서 주목할 점은 Superview의 Margin에 보라색 View의 Margin이 더해지는 것이 아니라는 것이다.
겹치는 Margin 중 가장 큰 값을 사용하고 나머지는 무시된다.

즉, 보라색 View의 Leading Margin이 30pt 일 때는 무시됐다가
100을 초과하는 순간 Superview의 Margin 대신 View의 Margin이 사용된다.

보라색 View의 Leading Margin을 Safe Area에서 Superview 기준으로 변경하고,
Relative Margin 속성과 값을 0으로 지정한다.
이렇게 되면 보라색 View 안의 흰색 View는 항상 Super View의 Margin에 보라색 View의 Margin을 더한 것만큼 떨어지게 배치된다.
즉, 이 경우 Preserve Superview Margin이 아무런 의미를 가지지 않는다.

다시 보라색 View의 Margin을 기본 상태로 되돌리면 Leading은 8pt로 적용된 반면
Trailing은 superview의 Margin을 그대로 사용하는 것으로 보인다.
방금 전 leading은 수정한 반면 trailing은 여전히 Bound를 기준으로 배치되어있기 때문에 Root View Margin의 영향을 받는다.

 

Layout Guide

iOS 7에서 새로운 Layout 방식이 도입되면서 View Controller는 Full Screen Layout을 사용하도로 변경되었다.
System View가 View Controller 위에 자리하게 되면서 System View와 겹치지 않도록 View를 배치하는 것이 중요해졌다.
해당 문제를 쉽게 해결하기 위한 방법 법이 Layout Guide다.

Xcode 최신 버전들은 기본적으로 iOS 11에 도입된 Safe Area Guide를 사용하도록 되어있다.
우선 사용하는 법을 알아보고 이전 버전에서 마이그레이션 하는 방식을 확인해 보도록 한다.

Layout Guide

Scene에 추가되어 있는 것은 top layout guide와 bottom layout guide이다.
이 둘은 현행 버전들에선 Safe Area로 대체된 것으로 얼핏 보면 차이를 느끼기 힘들다.

우선 top, bottom, leading, trailing 모두 superview의 bound를 기준으로 0의 제약을 추가한다.
이에 따라 Navigation Bar, Tap Bar 할 것 없이 모든 영역에 걸쳐 View가 표시된다.
배경으로 쓰는 경우엔 문제가 없지만 나머지 경우엔 System UI를 고려해야 한다.
iOS 11 이전에서는 top과 bottom 제약을 top layout guide, bottom layout guide에 맞춰 추가해야 한다.

Size Inspector에서 해당 제약을 선택해 superview 기준에서 top layout guide 기준으로 제약을 수정한다.
bottom도 같은 방식으로 변경한다.
이렇게 되면 System UI와 겹치지 않는 영역을 기준으로 표시된다.

상단과 하단의 공백이 top/bottom layout guide이다.
이 둘은 System UI가 업데이트됨에 따라 자동으로 조절되기 때문에 이 영역에 신경을 쓸 필요가 없다.
단, 해당 영역이 top, bottom, height Anchor를 가지고 있음을 기억해야 한다.

상단 제약을 확인해 보면 top layout guide의 bottom을 기준으로 제약이 추가됐음을 알 수 있다.
이는 bottom layout guide도 마찬가지이다.
즉, interface builder를 사용하는 경우 적합한 Anchor를 자동으로 선택해 추가하기 때문에 문제가 없다.
단, 코드로 추가하는 경우 주의가 필요하다.

레이아웃 가이드 마이그레이션 (Layout Guide Migration to Safe Area)

해당 방식들은 iOS 10까지 사용하던 방식이다.
앞서 말했듯 iOS 11부터는 Safe Area Layout Guide를 사용한다.
이전 방식은 이후 사용되지 않기 때문에 이를 적절히 반영해야 한다.

layout guide는 view Controller의 속성이기 때문에 Root View와 같은 계층에 포함되어있다.

Interface Builder

File Inspector에서 Use Sage Area Layout Guides를 활성화하면 해당 Layout Guide들이 Safe Area로 변경된다.

  • Layout Guide들은 View Controller의 속성이었지만 Safe Area는 View의 속성이다.
  • Safe Area는 구분되지 않은 하나의 영역이다.

따라서 top, bottom 제약들이 Safe Area 기준으로 변경되고,
Safe Area의 영역은 주황색 View의 영역과 일치하게 되기 때문에
Top layout guide의 bottom Anchor와 View의 Top을 기준으로 삼던 부분이
Safe Area의 top과 View의 top을 기준으로 삼도록 변경된다.

지금처럼 interface builder를 통해 작업하는 경우 iOS 11 이상에서는 Safe Area로 작동하고,
미만의 버전에서는 기존의 방식대로 작동한다.
하지만 코드를 사용하는 경우 이야기가 달라진다.

Code

//
//  LayoutGuideWithCodeViewController.swift
//  Margin and Guide Practice
//
//  Created by Martin.Q on 2021/12/14.
//

import UIKit

class LayoutGuideWithCodeViewController: UIViewController {
	@IBOutlet weak var orangeView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
		
		orangeView.translatesAutoresizingMaskIntoConstraints = false
		
		if #available(iOS 11.0, *) {
			orangeView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
			orangeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
			orangeView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
			orangeView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
		} else {
			orangeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
			orangeView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
			orangeView.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
			orangeView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
		}
    }
    

}

위와 같은 방식으로 구현하게 되는데,

orangeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true

orangeView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true

top과 Bottom의 제약을 적용하는 법이 조금 다르다.
첫 번째 라인에서 사용하는 safeAreaLayoutGuide는 iOS11 이상에서만 사용할 수 있는 속성이기 때문에
이전 버전에서는 두 번째와 같이 topLayoutGuide 속성에 접근해 적용해야 한다.

Safe Area를 통해 추가된 Inset은 별도의 속성을 통해 확인할 수 있다.

print(view.safeAreaInsets)

Auto Layout 없이 직접 배치하거나 Frame을 직접 계산해야 할 때 사용할 수 있다.

Safe Area 영역은 자동으로 계산되지만 별도의 속성으로 변경하는 것도 가능하다.

additionalSafeAreaInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

additionalSafeAreaInsets 속성에 변경할 값들을 전달하는 방식이다.

EventViewController Class의 응용할만한 메서드

override func layoutMarginsDidChange() {
	super.layoutMarginsDidChange()
	
	if #available(iOS 11.0, *) {
		let target = directionalLayoutMargins
	} else {
		let target = layoutMargins
	}
}

layoutMarginsDidChange 메서드는 이름 그대로 View의 Layout Margin이 변경될 때마다 호출된다.
해당 메서드에서 Layout Margin에 접근하는 경우 버전에 따라 방식을 달리해야 한다.

override func safeAreaInsetsDidChange() {
	super.safeAreaInsetsDidChange()
}

safeAreaInsetsDidChange 메서드는 iOS 11부터 사용할 수 있는 메서드이다.
해당 메서드는 View의 Safe Area의 크기가 변경될 때마다 호출된다.

override func viewLayoutMarginsDidChange() {
	super.viewLayoutMarginsDidChange()
}

viewLayoutMarginDidChange 메서드도 iOS 11부터 사용 가능한 메서드이다.
Root View의 Margin이 변경될 때마다 호출된다.

override func viewSafeAreaInsetsDidChange() {
	super.viewSafeAreaInsetsDidChange()
}

ciewSafeAreaInsetsDidChange 메서드도 iOS 11부터 사용 가능하다.
Root View의 Safe Area 크기가 변경될 때마다 호출된다.
특히 화면이 표시될 때 초회에 한해 호출되기도 한다.