본문 바로가기

학습 노트/iOS (2021)

163. Constraints with Code #1

Constraints with Code #1

제약을 코드를 통해 적용하는 법에 대해 정리한다.

제약은 NSLayoutConstraint 클래스로 구현돼있고, 해당 클래스의 멤버를 통해 제약을 활용할 수 있다.
Interface Builder를 통해 제약을 추가하는 것보다 어렵고 많은 코드를 입력해야 하지만,
동적으로 적용되는 제약을 자유롭게 적용할 수 있다.

제약을 추가하는 데에는 두 개의 멤버가 제공된다.
constraints(withVisualFormat:) 메서드는 Visual Format Language를 통해 제약을 생성할 때 사용된다.
init(item:) 메서드는 가장 기본적인 제약을 생성할 때 사용된다.

item1.attr = multiplier * item 2.attr + constant

이전에 다뤘던 제약 공식의 모든 변수를 파라미터로 받아야 하기 때문에 생성자가 매우 긴 편에 속한다.
또한 NSLayoutConstraint.Attrubute, NSLayoutConstraint.Relation 열거형에 어떤 멤버가 선언됐으며,
어느 시점에 사용돼야 하는지에 대해서 파악하고 있어야 한다.

Visual Format Language를 사용하면 여러 제약을 추가할 수 있고, 제약 코드의 가독성이 높아지지만, 새로운 문법을 다뤄야 한다.
또한, 문자열을 통해 전달하기 때문에 오타로 인한 오류의 가능성도 높고, 디버깅도 어렵다.

NSLayoutConstraint

isActive 속성은 제약의 활성화 상태를 나타내는 플래그이다.
추가된 여러 제약 중, 활성화된 제약들만이 실제로 계산에 사용된다.
Auto Layout 초기에는 addConstraint, removeConstraint를 통해 제약을 직접 추가하거나 삭제했지만,
Adaptive Layout 도입 이후부터는 isActive 플래그를 통해서 활성화 상태를 변경할 수 있게 됐다.
아래의 activate와 deactivate 메서드는 다수의 제약을 활성화하거나 비활성화하는 경우 사용한다.

해당 멤버들은 제약 공식에 사용하는 변수들과 동일하다.
constant를 제외한 모든 멤버는 제약을 생성한 후 변경할 수 없다.
제약을 동적으로 변경해야 한다면 제약을 삭제하고 다시 생성하는 것보다는 constant를 변경하는 것이 성능에는 도움이 된다.

firstAttribute와 secondAttribute의 형식을 보면 NSLayoutConstraint.Attribute 형식이다.

해당 형식은 열거형이며 top, bottom, leading, trailing 등의 여러 속성이 선언돼있다.
interface Builder에서는 Align 메뉴와 Constraint 메뉴를 통해 추가할 수 있었지만,
코드를 통해 추가할 때는 위와 같은 속성들로 명시적으로 지정해야 한다.

relation 멤버의 형식은 NSLayoutConstraint.Relation 형식이다.

마찬가지로 열거형이고, lessThanOrEqual, equal, greaterThanOrEqual 멤버로 구성돼있다.

priority는 Interface Builder에서 0 ~ 1000 사이의 정수 값으로 설정했던 것과는 달리,
UILayoutPriority 구조체로 설정해야 한다.
해당 구조체에는 자주 사용하는 우선순위가 required, defaultHigh 등의 타입 프로퍼티로 선언돼있다.
해당 프로퍼티를 사용하거나 생성자로 원하는 우선순위를 생성할 수 있다.

NSLayoutAnchor

iOS 9 이상부터는 제약을 더 쉽게 생성할 수 있도록 NSLayoutAnchor Class를 통해 다양한 메서드를 제공한다.
NSLayoutConstraint를 직접 생성하는 것에 비해 짧은 코드로 제약을 생성할 수 있고, 코드의 가독성도 높아진다.
또한, Type checking을 통해 잘못된 제약으로 인한 오류를 방지한다.
해당 방식에서는 View와 Layout이 제공하는 다양한 Anchor 속성과 메서드를 사용해서 제약을 생성한다.

// Creating constraints using NSLayoutConstraint
NSLayoutConstraint(item: subview,
                   attribute: .leading,
                   relatedBy: .equal,
                   toItem: view,
                   attribute: .leadingMargin,
                   multiplier: 1.0,
                   constant: 0.0).isActive = true

NSLayoutConstraint(item: subview,
                   attribute: .trailing,
                   relatedBy: .equal,
                   toItem: view,
                   attribute: .trailingMargin,
                   multiplier: 1.0,
                   constant: 0.0).isActive = true


// Creating the same constraints using Layout Anchors
let margins = view.layoutMarginsGuide

subview.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
subview.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true

위는 간단한 예제이다.
위는 NSLayoutConstraint를 사용한 코드이고,
아래는 같은 내용을 NSLayoutAnchor를 사용해서 구현한 것을 비교해 볼 수 있고,
View와 Margin이 제공하는 Anchor 속성을 사용하고 있다는 것을 확인할 수 있다.

이를 상속한 세 클래스는 위와 같다.

NSLayoutXAxisAnchor 클래스는 수평 방향의 제약을 추가할 수 있는 메서드를 제공하고,
NSLayoutTAxisAnchor 클래스는 수직 방향의 제약을 추가할 수 있는 메서드를 제공한다.
NSLayoutDimension 클래스는 width와 height 제약을 추가할 수 있는 메서드를 제공한다.

제공되는 디버깅 멤버는 위와 같다.

constriantsAffectingLayout 속성은 Anchor와 관련된 제약을 NSLayoutConstraint 배열로 반환한다.
hasAmbiguousLayout 속성은 Anchor와 관련된 제약 중 잘못된 제약이 존재하는지를 나타내는 플래그다.
name과 item 속성은 Anchor를 식별하는 용도로 사용한다.

코드를 통해 제약을 추가하는 방법은

  • NSLayoutConstraint
  • Visual Format Language
  • NSLayoutAnchor

세 가지로 크게 나눌 수 있다.

NSLayoutConstraint는 모든 제약을 생성할 수 있지만,
코드가 복잡해지는 단점이 있다.

Visual Format Language는 단순한 방식으로 제약을 추가할 수 있고, 한 번에 다수의 제약을 추가할 때 편리하다.
하지만, 별도의 문법을 사용해야 한다는 부담이 존재하고, 이를 통해 표현할 수 없는 제약도 존재한다.

위의 두 방식은 Compile Time에 에러를 판단할 수 없기 때문에 디버깅이 어려운 편에 속한다.

NSLayoutAnchor는 Type Checking을 통해 제약의 문제점을 Compile Time에 발견할 수 있다.
위의 두 방식에 비해 가독성이 높고, 적은 양의 코드로 동일한 제약을 생성할 수 있다.

Visual Format Language

"<expr>"
"H:<expr>"
"V:<expr>"

Visual Format Language는 수직 또는 수평 방향 제약을 하나의 문자열로 표현한다.
수평 방향이 기본값이고, 대문자 H와 V를 사용해 원하는 방향을 지정할 수 있다.
이때 H와 V 뒤에는 반드시 Colon이 붙어야 한다.
즉, 수평 방향의 제약을 나타낼 때는 'H:'가 생략된 형태로 사용하는 경우가 많고,
수직 방향의 제약을 나타낼 때는 'V:'로 표현식을 시작한다.

|

Vertical Bar는 Super View를 나타낸다.

[viewName]

View는 항상 Squar Bracket으로 나타낸다.
View의 이름은 문자열이 돼야 하고, NSLayoutConstraint 클래스가 제공하는 메서드를 호출할 때 Dictionary 형태로 전달해야 한다.

|{viewName}|

View가 여백 없이 상위 View의 전체를 채우는 문장은 위와 같다.

[viewName(==100)]

View의 이름 뒤에는 다양한 Predecate가 올 수 있다.
항상 괄호로 묶어서 표현해야 하고, 두 개 이상을 표현할 때는 ', '를 사용해 구분한다.
위의 문장은 View의 너비를 100pt로 고정하는 제약이다.
또한 Equal인 경우 생략할 수 있다.

V:[viewName(==100)]

높이 제약은 위와 같이 표현한다.

[viewName(>=100@750)]

제약의 우선순위는 '@'와 숫자의 조합으로 표현한다.

["defaultHeight":100]
[viewName(==defaultHeight)]

문자열 내에서 사용하는 숫자는 다른 변수로 치환하는 것이 가능하다.
Dictionary를 생성하고, defaultHeight에 100을 저장하고 이를 사용하는 것이 가능하다.

[v1][v2]

두 개의 View를 나란히 표시하면 여백 없이 이어서 배치된다.

[v1]-[v1]

View 사이에 '-'을 추가하면 두 View 사이의 기본 여백이 추가된다.

[v1]-100-[v2]

여백의 크기를 직접 지정하고자 한다면 위와 같이 표현할 수 있다.
v1과 v2 사이의 여백은 100pt가 된다.

"|-[titleLabel(>=100@750)]-10-[inputField]-10-[actionButton(==titleLabel)]-|"

종합하면 위의 문장은

titleLabel, inputField, actionButton 이라는 이름의 세 가지 View가 포함돼있다.
titleLabel 왼쪽에는 기본 여백이 추가되고, 최소 너비는 100pt이며, 너비 제약의 우선순위는 750이다.
titleLabel과 inputField 사이에는 10pt의 여백이 추가된다.
titleLabel과 actionbutton 사이에는 10pt의 여백이 추가되고, titleLabel의 너비 제약을 공유한다.
actionButton의 오른쪽에는 기본 여백이 추가된다.

 

'학습 노트 > iOS (2021)' 카테고리의 다른 글

165. Constraints with Code #3  (0) 2021.12.19
164. Constraints with code #2  (0) 2021.12.17
162. Auto Layout Practice #2  (0) 2021.12.16
161. Adaptive Layout  (0) 2021.12.16
160. Auto Layout Practice #1  (0) 2021.12.15