오토레이아웃
오토레이아웃이 도입되기 이전에도 코코아 및 코코아터치에서는 상위뷰나 윈도가 크기가 변하는 경우에 하위 뷰들의 크기가 그에 따라 어떻게 변할 것인지를 결정해주는 방법이 있었다. 흔히 spring & struts라 불리는 오토리사이징 마스크가 그것이다.
하지만 오토 리사이징 마스크는 한 가지 문제가 있는데, 그것은 오토 리사이징 마스크는 수퍼뷰와 서브뷰 둘 사이의 관계만을 정의하기 때문에 이를 따라 뷰의 크기나 위치가 움직일 때 서브 뷰 간의 레이아웃이 흐트러질 수 있는 가능성이 매우 많다는 것이다.
그리하여 예전에는 화면의 회전이나 키보드가 올라오는 등의 과정이 발생하면[^0-1] 뷰의 크기가 전환되는데 이때 변환된 크기에 따라서 다른 레이아웃을 갖도록 코드상에서 이를 일일이 수정해주어야 했다.
오토레이아웃
오토 리사이징 마스크가 한정된 몇 가지 요소에 의존할 수 없었던 것 때문에 명확한 한계를 가지고 있었고, 이를 보완하기 위해 등장한 오토레이아웃은 뷰의 프레임보다는 뷰의 관계에 대해서 접근하는 방식으로 패러다임 전환을 시도했다. 오토레이아웃은 두 뷰의 관계를 통해서 레이아웃을 결정하는 것이며, 따라서 일련의 제한요소(constraint)등을 정의하여 내적 / 외적인 레이아웃 변경 요인에 대해 전향적으로 대응할 수 있게 한다.
제한 요소
뷰 계층 구조의 레이아웃은 일련의 선형 방정식들에 의해서 결정되며, 이 때 하나의 방정식은 하나의 제한요소를 가리킨다.
위 그림은 애플 개발자 문서 중 오토레이아웃 가이드에서 가져온 것이다. 이는 제한 요소는 두 뷰의 관계, 그 중에서 특정한 제약 요소를 기술하는 것임을 보여준다.
- 오른쪽 red 뷰의 왼쪽 지점은 왼쪽 blue 뷰의 오른쪽 끝 지점에 0.8 만큼의 오프셋을 둔 지점과 일치해야 한다.
대부분의 제한 요소는 두개의 UI 요소간의 관계를 정의하나 하나의 요소의 두 속성간의 관계를 정의할 수도 있다. (예를 들어 어떤 뷰의 높이는 가로폭의 절반이라거나 하는 식)
오토레이아웃 속성
오토 레이아웃에서 ‘속성'(attributes)이란 제한 요소의 적용을 받는 어떤 기능을 의미한다. 여기에는 보통 4개의 변과 폭/높이 그리고 가로/세로 방향의 중심이 온다. 이러한 모든 요소들은 NSLayoutAttributes
enum타입에 정의되어 잇다.
물론 모든 속성들이 한꺼번에 다 정의될 필요도 없다. (그렇게 하기가 불가능한 경우도 많고) 오토레이아웃을 사용할 때의 목표는 오직 하나의 가능한 솔루션만을 갖는 일련의 방정식들을 정의내리는 것이다. 여기에는 개별 공식들이 충돌하여 솔루션을 만들지 못하는 경우도 있을 수 있고, 반대로 2개 이상의 솔루션을 만드는 모호한 경우가 있을 수도 있다.
일반적으로 2개의 축방향에 대해서 해당 방향으로의 뷰의 위치와 크기를 정의하는 것으로 오토레이아웃을 적용할 수 있다. 하지만 하나의 뷰의 레이아웃을 결정하는 공식 세트가 반드시 하나인 것은 아니다. 아래는 어떤 서브 뷰의 가로 위치와 크기를 결정하는 예이다.
- 첫번재 예는 왼쪽 여백과 폭을 정의했다. 이는 뷰가 고정된 폭을 가지며, 부모 뷰의 크기가 변경될 때에는 오른쪽 여백만 변경하며 뷰의 위치와 크기가 변하지 않음을 암시한다.
- 두번째 예는 왼쪽 및 오른쪽 여백을 고정값으로 정의했다. 이는 뷰의 폭이 부모뷰의 폭에서 특정한 오프셋 값만큼 뺀 양으로 정이된다는 것이다. 부모뷰의 크기가 변경될 때 서브 뷰는 여백의 크기를 유지한채로 폭이 변경된다.
- 세번째 예는 부모 뷰아 중심점을 맞추고 왼쪽 여백을 고정했다.
두번째와 세번째 설정은 설정한 내용은 다르지만 외관상 동일하게 동작한다.
IB를 통한 오토레이아웃 설정 법
건너뛴다.
주요 정책
다음의 가이드라인을 참고하라. 물론 각 규칙에 대한 예외는 어김없이 존재하겠지만, 이러한 규칙을 참고하는 것은 성공적인 접근을 도와준다.
- 뷰의 기하학적 정의를 frame, bounds, center를 통해서 정의하지 말 것
- 가능하다면 스택뷰를 사용할 것. 스택뷰는 콘텐츠의 레이아웃을 자동으로 관리하며 따라서 제한요소에 필요한 로직을 대거 줄여줄 수 있다.
- 뷰의 제한 요소는 가장 가까운 뷰와 관련지어 생성할 것
- 뷰에 고정폭, 고정높이를 적용하는 것을 피할 것
- 제한 요소를 설정하는데 어려움을 겪는다면,
Pin
,Align
도구를 활용할 것. 이는 컨트롤 드래깅(우클릭 드래깅)보다는 느리지만, 정확한 조정에 도움을 준다. - 프레임 업데이트를 주의깊게 사용할 것. 부정확한 설정 후에 이 동작을 실행하는 것은 예기치 못한 결과를 만들 수 있다.
left
,right
대신에 항상leading
,trailing
을 사용할 것- iOS에서 뷰의 엣지를 뷰 컨트롤러의 루트 뷰에 대응시킬 때 다음을 고려할 것
- 수평 제한: 대부분의 컨트롤에 대해서 레이아웃 마진값에 대해 0 값을 사용하면 시스템이 알아서 적절한 여백을 생성해준다.
- 텍스트 객체가 뷰의 폭을 가득 메울 가능성이 있다면 레이아웃 마진 대신에 가독성 콘텐츠 가이드를 사용하라
- 루트 뷰의 변에서 변을 채우는 콘텐츠를 정의하려면 뷰 자체의
leading
,trailing
에 0 포인트로 맞춰라 - 수직 제한: 뷰가 바 아래로 확장되는 경우에
top
,bottom
마진을 사용하라.
- 코드상에서 뷰를 초기화하는 경우 뷰의
translatesAutoresizingMaskIntoConstraints
값을false
로 바꿔라. 기본적으로 시스템에 의해 이 값은true
로 정해지고 그러면 오토레이아웃 제한자를 설정하는 것이 제대로 동작하지 않는다. - macOS와 iOS는 레이아웃을 계산하는 방식이 다르다.
스택뷰
스택뷰를 쓸 수 있으면 사용해라. 이런 복잡한 레이아웃도 사실 스택뷰를 쓰면 쉽게 정리할 수 있다.

UIStackView를 이용하여 구성한 레이아웃
코드상에서 제한요소를 만들기
사실 가능하다면 인터페이스 빌더를 사용해서 오토레이아웃 제한 설정을 만드는 것이 좋다. 인터페이스 빌더는 제한요소를 시각화하고 편집/관리하며 디버깅할 수 있는 많은 도구를 제공한다. 제한 요소들을 분석함으로써 인터페이스 빌더는 디자인 타임에서 발생하는 많은 일반적인 오류들을 찾아내고 자동으로 고칠 수 있다.
또한 기존에는 프로그래밍적으로만 제어할 수 있었던 뷰의 가변 개수에 대한 레이아웃 처리도 스택뷰를 사용하여 IB에서 구성할 수도 있다.
만약 코드상에서 오토레이아웃을 관리하겠다면 여기에는 크게 세가지 접근법이 있다.
1. 앵커를 사용하기
NSLayoutAnchor
는 제한요소를 만들기 위한 유틸리티 클래스로 사용하기 쉬운 이터페이스를 제공하여 다양한 제약 요소들을 생성할 수 있다. 대부분의 시각 요소들은 이러한 앵커들을 이미 가지고 있으며, 이들로부터 제약요소를 생성할 수 있다.
let margins = view.layoutMarginsGuide
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).aotive = true
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).active = true
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0)
위 코드는 myView
의 좌우를 기본 레이아웃 마진을 사용하여 부모 뷰에 딱 고정한다. 그리고 myView
의 높이를 가로의 2배로 사용한다.
다음의 예를 실제로 만들어보자.
- 컨테이너 뷰 안에 3개의 뷰가 들어있다.
- 빨간뷰와 파란뷰는 같은 너비이며 양쪽끝에는 표준 마진, 가운데는 8만큼의 공간을 띄운다.
- 초록색은 마진을 제외한 전체폭이며, 가운대로 정렬된다. 높이는 빨간색 뷰와 같은 높이를 주며, 컨테이너 내부 마진을 제외하고 8만큼의 여백을 갖는다.
위 조건을 코드로 표현하면 아래와 같다.
let containerView = UIView(frame: CGRect(x:0, y:0, width: 300, heigh: 400))
containerView.backgroundColor = UIColor.white
//: 뷰 세 개를 만들고 오토레이아웃을 적용할 준비를 한다.
let redView = UIView()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(redView)
let blueView = UIView()
blueView.backgroundColor = UIColor.blue
blueView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(blueView)
let greenView = UIView()
greenView.backgroundColor = UIColor.green
greenView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(greenView)
//: 컨테이너뷰로부터 표준 마진을 사용한다.
let margins = containerView.layoutMarginsGuide
//: `redView`의 위치는 위, 왼쪽, 폭, 높이를 결정해준다.
redView.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
redView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
redView.widthAnchor.constraint(equalTo: margins.widthAnchor,
multiplier: 0.5, constant: -4.0).isActive = true
readView.heightAnchor.constraint(equalTo: margins.heightAnchor,
multiplier: 0.5, constraint: -4.0).isActive = true
//: `blueView`는 좀 더 쉬운데 `redView`를 따라가기만 하면 된다.
blueView.topAnchor.constraint(equalTo: redView.topAnchor).isActive = true
blueView.widthAnchor.constraint(equalTo: redView.widthAnchor).isActive = true
blueView.heightAnchor.constraint(equalTo: redView.heightAnchor).isActive = true
blueView.leadingAnchor.constraint(equalTo: redView.trailingAnchor, constant: 8).isActive = true
//: `greenView`는 `redView`의 아래쪽으로 전체 폭을 사용한다.
greenView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 8).isActive = true
greenView.conterXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
greenView.bottomAnchor.containerView(equalTo: margins.bottomAnchor, constraint: -8).isActive = true
- Layout Contraints 를 사용하기
앵커를 사용하는 1의 문법들은 모두 조금 더 쓰기 쉬운 인터페이스를 이용해서 제약 요소값들을 생성하는 데, 이를 직접 이용하는 방법이 있다. 이 방법은 매우 무식하며 가독성도 떨어지고 그만큼 실수하기도 쉽다. 따라서 매우 추천하지는 않는다.
NSLayoutConstraint(item: myView, attribute: .leading, relatedBy: .Equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true
- 비주얼 포맷 사용하기
비주얼 포맷 언어는 마치 아스키 아트처럼 보이는 문자열을 이용해서 제약요소값들을 생성하는 방식이다.
- 오토레이아웃의 디버깅은 콘솔을 통해 비주얼 포맷을 사용하여 출력한다. 따라서 디버깅시에 사용되는 포맷과 생성에 사용하는 포맷이 일치한다.
- 비주얼 포맷을 사용하면 여러 제약 요소를 한 번에 만들 수 있다.
- 유효한 제약요소들만이 만들어진다. (단, 모든 필요한 제약요소가 다 만들어지는 것은 아니다.)
- 완전성보다는 좋은 시각화에 집중한 방식이다. 따라서 일부 제약요소는 이 방식으로 만들 수 없다.
- 단 포맷은 컴파일 타임에 체크할 수 없다. 실행하여 확인할 수만 있다.
어쨌든 이 방식은 다음과 같은 식으로 사용된다.
let views = ["myView": myView]
let formatString = "|-[myView]-|"
let constraints = NSLayoutConstraint.constraintsWithVisualFormat(formatString,
options: .AlignAllTop,
metrics: nil,
views: views)
NSLayoutConstraint.activateConstraints(constraints)
비주얼 포맷은 뷰의 한쪽 끝에서 끝까지를 다 정의해야 하는 것은 아니다. 연관있는 뷰들만 포함하는 일부분을 만들 수 있다. 이렇게해서 만들어지는 결과는 Array<NSLayoutConstraint>
타입이므로 이들 결과를 합한 다음에 NSLayoutContraint.activateContraints(_:)
를 이용해서 한꺼번에 활성화할 수 있다.
비주얼 포맷을 사용하여 위 세개 뷰의 위치를 정의한 코드는 다음과 같다.
let views = ["redView": redView,
"blueView": blueView,
"greenView": greenView]
let format1 = "V:|-[redView]-8-[greenView]-|"
let format2 = "H:|-[redView]-8-[blueView(==redView)]-|"
let format3 = "H:|-[greenView]-|"
var constraints = NSConstraint.constraints(withVisualFormat: format1,
options: alignAllLeft,
matrics: nil,
views: views)
constraints += NSConstraint.constraints(withVisualFormat: format2,
options: alignAllTop,
matrics: nil,
views: views)
constraints += NSConstraint.constraints(withVisualFormat: format3,
options: []
matrics: nil,
views: views)
NSConstraint.activateConstraints(constraints)