본문 바로가기

학습 노트/iOS (2021)

157 ~ 158. Constraint

Constraint

제약은 UI를 구성하는 요소 사이의 관계를 설정하고 Layout 시스템은 제약을 기반으로 최종 프레임을 계산하고 배치한다.

item1.attr = multiplier * item2.attr + constant

이에는 위의 공식이 사용된다.

Equation

Scene에 생성되어있는 버튼에 다음과 같이 제약을 추가한다.

새롭게 추가된 왼쪽 여백 제약은 공식으로 다음과 같이 표현할 수 있다.

button.leading = 1.0 * view.leading + 50

상단의 여백은 공식으로 다음과 같이 표현할 수 있다.

button.top = 1.0 * view.top + 100

공식의 item1과 item2는 제약 대상을 의미한다.
보통은 View를 지정하지만 경우에 따라 Layout Guide를 지정할 수도 있고, 너비와 높이 제약의 경우 item2를 지정하지 않기도 한다.
attr은 제약을 적용할 속성을 의미한다.
margin를 지정하는 속성으로는

  • leading
  • top
  • trailing
  • bottom
  • left
  • right

가 존재한다. 이들은 두 View 사이의 거리를 나타낸다.
이들 중 left와 right만 사용 빈도가 낮고, leading과 trailing은 사용빈도가 가장 높다.

leading은 우리나라와 같은 좌횡식 문화권에서는 left와 같은 의미로 쓰인다.
하지만 아랍과 같은 우횡식 문화권에서는 right와 같은 의미로 쓰인다.
즉 leading은 글의 시작 부분을 의미하는 속성이다.

top과 bottom은 이름 그대로 위와 아래를 의미한다.

leading, top, trailing, bottom

제약을 추가하기 위해 Contraints 메뉴에 접근한다.
필드에는 기본적으로 가장 가까운 View와의 거리가 입력되어있는데, 필요에 따라 원하는 값을 지정해도 문제없다.
또한 옆의 화살표를 눌러 대상을 지정할 수도 있다.
이 또한 기본적으로 가장 가까운 View 혹은 Safe Area가 지정되어있다.
Safe Area는 상태바등의 System UI와 겹치지 않는 기준점으로,
지금처럼 RootView를 기준으로 제약을 추가하는 경우에 사용하는 것이 좋다.
모든 방향의 제약을 30으로 추가한다.

방금 추가한 제약들을 Document Outline에서 확인해 보면 View의 아래가 아닌,
Root View의 아래에 추가된 것을 확인할 수 있다.
Margin과 관련된 속성이나 View가 연관된 속성들은 가장 인접한 공통 View에 추가된다.
즉 방금 추가한 4개의 제약은 보라색 View와 Root View 사이의 제약이므로 가장 인접한 공통 View인 Root View에 추가된다.

width, height

너비와 높이 제약을 추가하는 경우 하나의 View에 고정된 Constant 값으로 설정하거나, 다른 View와의 관계나 비율을 추가할 수 있다.

주황색 버튼에는 고정된 높이와 너비 제약을 추가하고, 파란색 View는 주황색 버튼과 같은 높이와 너비를 가지도록 제약을 추가한다.

주황색 버튼에는 위와 같이 제약을 추가한다.
leading과 top에 100pt 만큼의 margin을 추가하고,
너비와 높이를 200pt로 설정했다.

파란색 View를 버튼과 겹치지 않도록 이동시킨 뒤 Constraints 메뉴를 보면 역시 자동으로 값이 입력되어있다.
해당 값은 현재 파란색 View에 가장 인접한 View와의 거리로, 버튼과 겹치지 않기 때문에 가장 인접한 Safe Area와의 거리를 나타낸다.

위치를 옮기고 다시 확인해 보면 이번에는 버튼과의 거리가 입력되어있다.
지금 상태에서 대상을 변경하고 싶다면 화살표를 클릭해 원하는 대상을 선택하면 된다.

top과 leading에 현재 값을 제약으로 추가하면 왼쪽 사진과 같이 빨간색으로 표시된다.
이는 파란색 View의 너비와 높이를 판단할 수 없기 때문이다.

파란색 View를 오른쪽 클릭으로 드래그해 버튼과 연결한다.
이후 Equal Widths와 Equal Heights를 선택해 추가한다.

Document Outline에서 추가된 제약조건들을 확인해 보면 버튼에 추가된 제약은 다른 View와 연관되지 않은 제약이다.
따라서 버튼의 아래에 추가되어있는 것을 확인할 수 있다.
하지만 파란색 View에 추가한 width, height 제약은 버튼과 연관된 제약이기 때문에
가장 인접한 공통 View인 Root View의 아래에 추가된다.

centerX, centerY

가운데 정령은 수직과 수평방향 모두 추가할 수 있고, Container를 기준으로 하거나 두 개 이상의 View를 정렬할 수 있다.

파란색 Label에 Root View의 정 가운데에 오도록 제약을 추가한다.

Alignment 메뉴에서 Horizontally in Container와 Vertically in Container를 선택해 적용하면 Label이 화면 정중앙으로 이동한다.
너비와 높이 제약이 존재하지 않기 때문에 에러가 발생해도 이상하지 않지만 지금은 문제가 없다.
Label과 같이 View의 내용을 통해 크기를 유추하는 경우 너비와 높이 제약을 생략할 수 있고,
이 경우 Intrinsic content size를 통해 View의 크기를 표현한다.
반면 초록색 View와 노란색 View는 크기를 유추할 내용이 존재하지 않기 때문에 반드시 너비와 높이 제약이 존재해야 한다.

Label의 아래에 있는 노란색 View에 위와 같이 제약을 추가한다.

오른쪽 마우스로 드래그해 Label과 연결한 뒤 Center Horizontally를 추가하면 두 View가 가운데 정렬된다.

초록색 View에는 위와 같이 제약을 추가한다.

이번엔 Label과 초록색 View를 동시에 선택하고

Alignment 메뉴에서 Vertical Centers를 선택하면 조금 다른 방식으로 정렬을 추가할 수 있다.

Document Outline을 확인해 보면 높이, 너비 제약과는 다르게 정렬 제약은 항상 공통 View에 추가된다.
즉 이번에는 모두 Root View 아래에 추가됐다.

aspectRatio

해당 제약은 View의 종횡비를 설정하는 제약이다.
종횡비는 높이나 너비를 기준으로 나머지가 계산되기 때문에 둘 중 하나는 추가되어있어야 한다.

View를 선택하고 화면의 중앙에 올 수 있도록 수직, 수평 정렬 제약과 Aspect Ratio 제약을 추가한다.
마치 높이와 너비 제약이 추가된 것처럼 보이지만 이것은 Aspect Ratio 제약이다.
또한 종횡비를 계산하기 위한 기준이 존재하지 않기 때문에 제약 오류가 발생한다.

너비 제약만 추가해 보면 모든 제약 오류가 사라지고, View의 높이는 너비와 Aspect Ratio를 기준으로 계산된다.
Aspect Ratio 제약을 추가할 때는 비율을 계산할 수 있는 기준이 있어야 한다는 것을 명심해야 한다.

baseline

4개의 Label 중 색이 같은 것들끼리 이름의 방식으로 정렬해 차이를 확인한다.

첫 번째 Label에는 위와 같이 제약을 추가한다.

두 번째 Label에서 오른쪽 클릭으로 드래그 첫 번째 Label과 연결하고, Horizontal Spacing과 Bottom을 적용한다.
해당 제약은 Frame을 기준으로 정렬된다.
leading, trailing, top, bottom 제약은 View의 내용에 관계없이 항상 Frame을 기준으로 정렬한다.

세 번째 Label을 선택하고 위와 같이 제약을 추가한다.

네 번째 Label을 오른쪽 마우스로 드래그해 세 번째 Label과 연결한 뒤, Horizontal Spacing과 First Baseline을 추가한다.
해당 제약은 View의 내용을 기준으로 정렬한다.

relation

제약을 추가할 때 두 항목 사이의 관계를 relation으로 지정하게 되는데 기본값은 Equal이다.
지금까지의 방식으로 제약을 추가하면 항상 Equal이 적용된다.
추가로 Greater than or Equal과 Less than or Equal로 지정하는 것도 가능하다.

가장 첫 번째 Label엔 leading, top, trailing에 20, bottom에 8, height가 100인 제약이 추가되어있다.

Size inspector에서 Height 제약을 찾아 더블클릭하고 Attribute에서 Relation을 Less Than or Equal로 변경한다.
Label의 높이가 내용을 표시하는데 필요한 Intrinsic Size로 변경된다.
이 경우에도 높이가 100보다 작거나 같다는 기준을 만족시킬 수 있기 때문이다.

Greater Than or Equal로 변경하면 다시 100pt로 표시되고, 다른 제약에 의해 100pt보다 커질 수 있다.

마지막 Label의 Bottom 제약을 20으로 설정하면 Greater Than or Equal로 설정한 첫 번째 Label의 높이가 100보다 큰 값으로 변한다.
나머지 Label의 높이는 Equal 100pt이기 때문에 항상 100pt로 표시되지만 첫 번째 Label은 100보다 큰 값으로 표시가 가능하다.

multiplier

multiplier는 제약 공식에서 두 번째 항목에 적용할 승수를 배수를 나타내고 기본값은 1이다.
쉽게 말해 제약을 추가할 때 비율을 지정할 수 있다.

주황색 View는 화면의 중앙에 위치하도록 수직 수평 중앙 정렬, bottom margin과 높이 240pt, 너비 120pt의 제약이 추가되어있다.

보라색 View는 주황색 View와 연결되어 수직 정렬과 주황색 View와 같은 너비와 높이를 가지도록 제약이 추가되어있다.

보라색 View의 제약을 선택하고 Edit을 누르면 Multiplier가 기본값인 1로 설정되어있는 것을 확인할 수 있다.
이것을 2로 변경한다.

보라색 View의 너비가 주황색 View의 너비의 2배가 됐다.
마찬가지로 높이의 배수를 0.5로 지정하면 주황색 View의 너비의 절반이 된다.
높이처럼 실수로 지정해도 문제는 없지만 정수로 지정하는 것이 계산 과정에서 발생하는 오차를 줄일 수 있어 유리하다.
즉, 제약의 대상을 뒤바꾼 다음 값을 변경하면 같은 결과를 얻을 수 있다.

제약의 Attribute에서 item의 Reverse Fisrt And Second Item을 선택하면 결과는 동일하지만 Multiplier가 2로 변경된다.
'주황색 View의 높이는 보라색 View의 높이의 2배이다.'와 '보라색 View의 높이는 주황색 View의 높이의 절반이다.'는
Auto Layout 입장에서 동일한 의미를 가진다.
마찬가지로 주황색 View의 Bottom 제약을 추가하는 것과 보라색 View의 Top 제약을 추가하는 것은 동일한 의미를 가진다.

constant

상수로 번역되어 변경할 수 없는 값으로 오해하기 쉽지만 언제든 변경 가능하다.
따라서 Runtime에 제약을 업데이트하는 용도로 자주 활용된다.

Scene에는 View가 하나 추가되어있고 높이 240pt, 너비 128pt, 화면 중앙에 표시되도록 수평, 수직 정렬이 추가되어있다.

width 제약의 constant에는 이미 값이 입력되어있다.
width와 height는 constant 자체가 최종 값으로 사용되기 때문에 해당 값을 변경하는 것으로 크기를 조절할 수 있다.

반면 Align 제약은 Constant가 0으로 되어있고,
해당 값을 변경하면 정렬 기준이 바뀐다.
예를 들어 30으로 변경하면 오른쪽으로 30pt만큼 이동하는 식이다.
음수를 사용하여 왼쪽으로 이동시킬 수도 있지만 가능하면 0이나 양수를 사용하는 것이 좋다.
마찬가지로 First item과 Seconf item을 반전시키면 된다.
제약에 따라 Constant가 다른 의미를 가지기도 하니 적용할 제약에 따라 확인하는 것이 좋다.

//
//  ConstantViewController.swift
//  Constraint Practice
//
//  Created by Martin.Q on 2021/12/12.
//

import UIKit

class ConstantViewController: UIViewController {
	@IBOutlet weak var greenView: UIView!
	
	@IBAction func updateAction(_ sender: Any) {
	}
	

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

Scene과 연결된 코드에는 update 버튼이 연결되어있다.
해당 버튼을 터치하면 View의 높이와 너비를 모두 100pt로 변경하도록 구현한다.

//
//  ConstantViewController.swift
//  Constraint Practice
//
//  Created by Martin.Q on 2021/12/12.
//

import UIKit

class ConstantViewController: UIViewController {
	@IBOutlet weak var greenView: UIView!
	
	@IBAction func updateAction(_ sender: Any) {
		var frame = greenView.frame
		frame.size.width = 100
		frame.size.height = 100
		greenView.frame = frame
	}
	

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

greenView의 Frame을 별도의 변수에 저장하고,
size 속성을 통해 height와 width를 변경해 다시 greenView의 Frame으로 변경한다.

하지만 위 방식대로는 Frame의 크기를 변경할 수 없다.
해당 방식은 Frame Based 방식에서 사용하는 방법이다.
Auto Layout에서는 제약을 통해 크기를 결정하기 때문에 제약을 변경해야 한다.

이번엔 greenView를 통해 제약에 접근하는 것이 아닌 제약 자체를 outlet으로 연결해 접근하는 방법을 사용한다.
이때 제약과 연결했다는 것을 알 수 있도록 명확한 이름으로 연결하는 것이 좋다.

//
//  ConstantViewController.swift
//  Constraint Practice
//
//  Created by Martin.Q on 2021/12/12.
//

import UIKit

class ConstantViewController: UIViewController {
	@IBOutlet weak var greenView: UIView!
	
	@IBOutlet weak var heightConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst: NSLayoutConstraint!
	
	@IBAction func updateAction(_ sender: Any) {
		heightConst.constant = 100
		widthConst.constant = 100
	}
	

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

정상적으로 View의 크기가 변경된다.

priority

우선순위는 1000 ~ 1 사이의 값으로 제약의 우선순위를 결정한다.
1000은 필수 제약을 의미하고 이보다 작은 값들은 모두 선택적 제약을 의미한다.
항상 우선순위가 높은 제약이 사용되고, 충돌하는 제약이 같은 우선순위를 가진다면 제약 오류가 발생한다.

Scene에는 같은 레이아웃을 가진 View들이 존재한다.
제약도 이전과 동일하다.
제약을 추가하는 경우 제약의 종류별로 하나씩 추가하는 게 일반적이지만 경우에 따라 두 개 이상의 같은 제약을 추가하는 것이 가능하다.

따라서 초록색 View에 width 제약을 하나 더 추가할 수도 있다.
위의 사진은 width를 100pt로 제한하는 제약을 하나 더 추가한 모습으로,
기존과는 조금 다르게 Greater than or Equal이 기본값으로 추가됐다.
이는 기존의 제약과 같은 제약을 추가하면 충돌이 발생하기 때문으로 Xcode가 의도적으로 회피하기 위해 다르게 적용한 것이다.

해당 제약은 다시 Equal로 변경한다.
해당 View에는 두 개의 width 제약이 추가되어있고, Constant가 서로 다르다.
우선순위마저 1000으로 같기 때문에 모호함이 발생해 제약 오류가 발생한다.

100pt 제약의 우선순위를 999로 변경하면 충돌이 사라지고, 해당 제약은 점선으로 표시된다.
지금 상태에선 우선순위가 높은 제약을 명확히 사용할 수 있으므로 모호함이 사라지고 제약 오류가 발생하지 않는다.

또한 Document Outline에서의 이름도 변경되어 '@우선순위'가 추가되어있다.

//
//  PriorityViewController.swift
//  Constraint Practice
//
//  Created by Martin.Q on 2021/12/12.
//

import UIKit

class PriorityViewController: UIViewController {
	@IBOutlet weak var greenView: UIView!
	@IBOutlet weak var heightConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst999: NSLayoutConstraint!
	
	@IBAction func toggleAction(_ sender: Any) {
	}
	
	

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

연결된 코드에 각각의 제약을 추가한다.
버튼을 터치하면 해당 제약들의 우선순위를 변경하도록 구현한다.

//
//  PriorityViewController.swift
//  Constraint Practice
//
//  Created by Martin.Q on 2021/12/12.
//

import UIKit

class PriorityViewController: UIViewController {
	@IBOutlet weak var greenView: UIView!
	@IBOutlet weak var heightConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst999: NSLayoutConstraint!
	
	@IBAction func toggleAction(_ sender: Any) {
		widthConst.priority = widthConst.priority.rawValue < 1000 ? UILayoutPriority(1000) : UILayoutPriority(999)
		widthConst999.priority = widthConst.priority.rawValue < 1000 ? UILayoutPriority(1000) : UILayoutPriority(999)
	}
	
	

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

priority는 정수로 되어있지 않고 priority 구조체로 되어있다.
따라서 생성자에 원시 값을 넘겨주어 UILayoutPriority 구조체의 형태로 전달해야 한다.

동작은 하지만 위와 같은 로그를 출력한다.
내용인 즉 필수 제약을 옵션 제약으로 변경하거나 그 반대로 변경하는 행위는 Auto Layout 입장에서 모호하다.
따라서 둘 중 하나는 필요 없는 제약이라고 판단해 이를 경고하는 셈이다.
따라서 동적으로 제약의 우선순위를 변경하는 경우 1000이 아닌 다른 값으로 우선순위를 지정해야 한다.

//
//  PriorityViewController.swift
//  Constraint Practice
//
//  Created by Martin.Q on 2021/12/12.
//

import UIKit

class PriorityViewController: UIViewController {
	@IBOutlet weak var greenView: UIView!
	@IBOutlet weak var heightConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst: NSLayoutConstraint!
	@IBOutlet weak var widthConst999: NSLayoutConstraint!
	
	@IBAction func toggleAction(_ sender: Any) {
		widthConst.priority = widthConst.priority.rawValue < 900 ? UILayoutPriority(900) : UILayoutPriority(800)
		widthConst999.priority = widthConst.priority.rawValue < 900 ? UILayoutPriority(900) : UILayoutPriority(800)
	}
	
	

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

필수 제약으로 설정되어있던 제약의 우선순위를 다른 값으로 변경하고 코드도 이에 맞춰 수정한다.
900과 800으로 변경했기 때문에 코드도 그에 맞게 수정했다.

의도한 대로 동작한다.
이를 사용해 동적으로 다양하게 변하는 UI를 구현할 수 있다.

우선순위를 설정할 때 Storyboard를 사용하면 정수를 사용할 수 있다.
하지만 코드를 사용하는 경우 구조체를 사용해야 한다.

extension UILayoutPriority {

    @available(iOS 6.0, *)
    public static let required: UILayoutPriority

    @available(iOS 6.0, *)
    public static let defaultHigh: UILayoutPriority // This is the priority level with which a button resists compressing its content.

    public static let dragThatCanResizeScene: UILayoutPriority // This is the appropriate priority level for a drag that may end up resizing the window's scene.

    public static let sceneSizeStayPut: UILayoutPriority // This is the priority level at which the window's scene prefers to stay the same size.  It's generally not appropriate to make a constraint at exactly this priority. You want to be higher or lower.

    public static let dragThatCannotResizeScene: UILayoutPriority // This is the priority level at which a split view divider, say, is dragged.  It won't resize the window's scene.

    @available(iOS 6.0, *)
    public static let defaultLow: UILayoutPriority // This is the priority level at which a button hugs its contents horizontally.

    @available(iOS 6.0, *)
    public static let fittingSizeLevel: UILayoutPriority // When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed.  UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation.  It's quite low.  It is generally not appropriate to make a constraint at exactly this priority.  You want to be higher or lower.
}

UILayoutPriority는 여러 속성이 지정되어있다.
대부분의 경우 해당 속성을 사용해 설정하고, 해당 속성들로 구현할 수 없을 때 정수로 지정한다.
예를 들어 첫 번째 속성인 required는 우선순위 1000을 의미한다.
defaultHeight는 750과 같고, defaultLow는 250과 같다.
fittingSizeLevel은 50과 같은데 잘 사용되지 않는다.

intrinsic content size

intrinsic size는 직역하면 '고유 크기' 정도로 번역할 수 있다.
Auto Layout은 View의 크기를 결정할 때 width와 height 제약을 확인한다.
두 제약이 존재하지 않는다면 다른 제약을 통해 크기를 계산할 수 있는지 확인한다.
그마저도 불가능하다면 View가 출력하는 내용을 통해 크기를 계산한다.
계산에 성공했다면 해당 크기로 View를 출력하고, 실패했다면 제약 오류가 발생한다.
마지막 방법인 'View가 출력하는 내용을 통해 크기를 계산한다.'에 해당되는 것이 Intrinsic size이다.
이 경우 content size layout constraint라는 제약이 추가된다.

사용하는 여러 Control 중에 Label이나 Switch처럼 Intrinsic size를 가진 경우도 있고,
Table View, Web View처럼 가지고 있지 않은 경우도 있다.

Scene에 추가된 다양한 Control들은 아무런 제약이 추가되어있지 않다.

Label에 top, leading 제약을 추가하면 왼쪽과 같이 변한다.
Label은 출력하는 text와 폰트를 기반으로 크기를 계산할 수 있고, 해당 크기를 Intrinsic size로 사용한다.
따라서 width와 height 제약을 추가하지 않아도 제약 오류가 발생하지 않는다.
다국어를 지원하는 경우 문자열의 길이가 길어져 cliping 되는 문제가 발생할 수도 있기 때문에
오히려 Label에 해당 제약들을 추가하는 것은 피해야 할 패턴 중 하나이다.

Button도 Intrinsic Size를 가지고 있다.
Button은 터치를 감지해야 하고, 이를 위한 최소한의 공간이 필요하다.
따라서 Button의 Title 출력을 담당하는 Label의 Intrinsic Size에 약간의 여백을 추가한 크기가 최종 Intrinsic Size가 된다.

이 차이는 Label과 Button의 Text를 모두 지우면 쉽게 확인할 수 있다.

Label은 사라진 반면 Button은 최소한의 공간이 필요하기 때문에 남게 된다.
이를 통해 Intrinsic Size를 사용할 것인지, width와 heifht 제약을 추가할 것인지를 결정해야 한다.

Switch에도 동일하게 제약을 추가한다.
Switch는 다른 Control들과 달리 항상 고정된 크기로 표시된다.

즉 이렇게 width와 height를 100pt로 고정하는 제약을 추가해도 Frame의 크기만 변경될 뿐 표시되는 크기는 이전과 동일하다.

Test Field에도 동일한 제약을 추가한다.
Text Field는 Border Style에 따라 다양한 높이를 가질 수 있는데, 기본 스타일에서의 높이는 고정값이다.
너비는 항상 입력된 문자열의 길이를 통해 결정된다.
즉, 지금은 입력된 값이 없기 때문에 최소 길이로 표시된 것이다.
Text Field가 입력된 값에 따라 유동적으로 너비를 갖는 것은 부 자연스럽기 때문에
너비 제약을 통해 고정하거나 수평을 기준으로 margin을 추가하는 것이 좋다.

Border Style을 첫 번째 스타일로 변경하면 제약 오류는 발생하지 않지만 값을 입력하는 것 자체가 불가능해진다.
터치를 입력받을 최소한의 공간이 없기 때문이다.
따라서 최소한의 높이와 너비가 확보되도록 제약을 추가하는 것이 좋다.

Slider에 같은 제약을 추가하면 오류가 발생한다.
Slider는 고정된 높이를 가지지만 너비는 정해져 있지 않다.
따라서 너비를 고정하도록 제약을 추가해 줘야 한다.
즉, Intrinsic Size가 항상 높이와 너비 둘 다를 만족하지는 않는다.
이 경우 값을 가지지 않는 나머지 속성을 직접 추가해야 할 필요가 있다.

마지막의 갈색 View에도 같은 제약을 추가하면 오류가 발생한다.
일반 View는 표시할 내용을 판단할 수 없기 때문에 크기를 반드시 지정해야 한다.

CHCR

제약의 우선순위와 같이 1000 ~ 1 사이의 값을 지정할 수 있는 우선순위를 가지고 있다.
해당 우선순위는 View를 배치할 수 있는 공간의 크기가 Intrinsic Size와 다른 경우
너비와 높이값이 커지거나 작아지는 우선순위를 지정한다.
수직과 수평 방향으로 하나씩 지정할 수 있다.

UI를 구성할 때 View의 Frame이 Intrinsic Size가 아닌 다른 제약들을 기반으로 계산되도록 구성됐다면
대부분의 경우 CHCR을 고려할 필요는 없다.
주로 Intrinsic Size를 가진 View가 동일한 선상에 두 개 이상 배치돼 있을 때 의미를 가진다.
또한 View에 추가된 다른 제약의 우선순위에 영향을 받을 수 있기 때문에 여러 우선순위를 동시에 고려해야 한다는 점이 난이도를 높인다.

Content Hugging

줄여서 CH라고도 부른다.
기본값은 수직과 수평으로 250이지만 Control의 종류에 따라 다를 수 있다.

Frame 확장에 대한 저항력을 의미한다.
CH의 우선순위가 1000이라면 가장 강한 저항력을 가지게 되고, 어떠한 경우에도 Intrinsic Size를 초과하지 않는다.
즉, 우선순위가 낮아질수록 저항력도 낮아지며 Intrinsic Size를 초과할 가능성이 높아진다.

Scene의 Label과 Tetx Field에는 수직상의 가운데 정렬과 일정 간격의 Margin이 추가돼있다.
두 Control의 너비는 각각의 Intrinsic Size와 Content Hugging을 통해 계산된 너비이다.

Size Inspector에는 CH를 지정할 수 있는 필드가 존재한다.
수직과 수평을 개별적으로 지정할 수 있고, 지금 상황에서는 수평만 다룬다.
Label의 수평 CH는 251이다.

반면 Text Field의 수평 CH는 250이다.
따라서 CH의 저항력이 높은 Label 대신 저항력이 낮은 Text Field의 너비가 확장된다.

즉 Text Field의 CH를 Label보다 높게 설정하면 Label의 저항값이 낮으므로 Label의 너비가 확장된다.

Label과 같은 251로 변경하면 제약 오류가 발생한다.
우선순위가 같아 모호함이 발생하기 때문이다.
이러한 경우를 Ambigus Layout이라고 하고,
이를 회피하기 위해 우선순위를 다르게 설정하거나, 고정된 너비 제약을 추가하거나, Equal width 제약을 추가하는 방법이 있다.

위와 같이 Label에 고정 너비 제약을 추가하면 우선순위가 1000인 제약이 추가되기 때문에 CH를 고려할 필요가 사라진다.
즉, Label은 200pt로 표시하고 Text Field가 나머지 영역을 채우는 식이다.

Compress Resistant

줄여서 CR이라고 부른다.
기본값은 수직과 수평으로 750이지만 Control의 종류에 따라 다를 수 있다.

CR은 축소에 대한 저항력이다.
우선순위가 1000이라면 어떤 경우에도 Cliping 없이 표시되지만
우선순위가 낮아질수록 저항력이 낮아 Cliping이 발생하거나 경우에 따라 아예 표시가 되지 않거나 오류가 발생할 수도 있다.

두 개의 Label은 일정 Margin으로 수직상의 중신에 위치하고, 같은 너비를 가질 수 있도록 Equal width 제약이 추가되어있다.

지금 상태에서 Equal width를 제거하면 너비를 가늠할 수 없기 때문에 제약 오류가 발생한다.

왼쪽 Label의 CR은 기본값이 750으로 지정되어있다.
마찬가지로 오른쪽의 Label도 같은 값이 750의 기본값을 가지고 있기 때문에 생기는 문제다.

해당 값을 751로 변경하면 왼쪽 Label의 저항력이 강해져 Cliping 없이 최대한 표시될 수 있도록 너비를 가지게 된다.
지금의 경우 최대한 활용해도 Cliping이 발생하므로 CR이 낮은 오른쪽 Label이 완전히 사라졌다.

반대로 오른쪽 Label의 CR을 752로 설정하면 상황이 역전돼 오른쪽 Label이 Cliping을 최소화하기 위해 너비를 확장한다.

현재의 너비와 높이는 Intrinsic Size에 해당되는 964pt, 21pt이다.
해당 크기보다 작은 크기인 300pt로 고정 너비 제약을 추가한다.

이 경우 너비 제약이 우선순위 1000을 가지기 때문에 CR을 고려할 필요가 없다.
하지만 이후 너비 제약의 우선순위가 변경된다면 주의가 필요하다.
너비 제약의 우선순위를 CR과 동일한 752로 변경한다.

이 경우 제약 오류가 발생하지는 않는다.
하지만 CR의 우선순위와 제약의 우선순위가 동일할 때는 CR이 우선순위가 높다.

Control이 많아질수록 굉장히 난해 해지는 경우가 많으므로 CR, CH, 제약의 우선순위를 함께 고려하는 경우는 최대한 회피해야 한다.