이 포스트는 다음의 글을 번역한 글입니다.
원글 :
iOSエンジニア必見!!iOSのレイアウトで押さえておきたいこと【総集編】
https://developers.eure.jp/tech/ios_layout_beginners/
안녕하세요! Couples의 iOS엔지니어 단(丹)입니다.
이번에는 iOS 개발자라면 반드시 집고 넘어가야할 View의 레이아웃에 대해서 정리해보겠습니다.
View의 레이아웃은 어플을 만드는데 있어 기본중의 기본이지만 깊은 이해가 없더라도 어플이 동작하게 만들수는 있습니다. 그러나, 퍼포먼스를 의식해야 하거나 설계를 제대로 하는데 있어서 View의 레이아웃을 이해하는 것은 필수 입니다. 레이아웃을 더 깊이 이해하는 것을 돕기 위해 이 글이 조금이나마 참고가 될수 있다면 좋겠습니다. 대상 독자는 초급자부터 중급자 대상입니다.
이 포스트에서는 Xcode 7.2.1, Swift 2.1을 사용하였습니다.
목차
- View와 ViewController의 레이아웃 사이클
- Constraints
- View의 updateConstraints
- View의 Intrinsic Content Size 란?
- content Hugging과 Compression resistance
- Intrinsic Content Size를 동적으로 변경하기
- ViewController의 updateViewConstraints
- 커스텀 뷰는 requiresConstraintBasedLayout를 사용하자
- Layout
- ViewのlayoutSubviews
- NSLayoutConstraint의 값을 갱신한 경우
- ViewControllerのviewWillLayoutSubviews와 viewDidLayoutSubviews
- View의 레이아웃 사이클 메소드 정리
- AutoLayout과 애니메이션
- 코드로 AutoLayout작성
- 코드로 AutoLayout을 작성할시 주의 점
- Couples에서 사용하고 있는 AutoLayout 라이브러리 「Cartography」에 대하여
- 끝으로
View와 ViewController의 레이아웃 사이클
View와 ViewController의 레이아웃 사이클은 반드시 집고 넘어가야 합니다. 어플의 백그라운드에서 어떻게 움직이고 있는지 이해하기 위해 레이아웃 사이클을 이해하는 것은 엄청나게 중요합니다. 이번에는 Subview가 하나도 없는 비어있는 ViewController를 준비합니다. 이 ViewController를 표시 했을 때 아래의 순서대로 ViewController와 ViewController가 갖고 있는 view의 각 메소드가 호출 됩니다.
설명이 필요한 메소드는 별도로 설명하겠습니다. 여기에서 대략적으로 3개의 스텝이 있다는 것을 기억해주십시오.
step 1. 제약(Constraints)
이 스텝에서 Autolayout의 제약(Constraints)을 갱신합니다. 제약(Constraints)의 갱신은 subview로부터 superview의 순서로 호출 됩니다.step2. 레이아웃(layout)
step1의 제약(Constraints)을 바탕으로 레이아웃을 실행합니다. 여기에서 view의 center와 bounds를 결정합니다. 레이아웃의 갱신은 superview로부터 subview의 순서로 호출됩니다.step 3. 그리기(Draw)
step2의 레이아웃후에 UIView의 drawRect(rect:CGRect)가 호출 됩니다. 이 그리기 스텝에서는 CoreGraphics를 사용하여 그리게 됩니다. Core Graphics에서 그리기가 필요한 어플은 그다지 많지 않다고 생각됨으로 본 포스트에서는 설명을 생략하고 있지만 반드시 학습해 보시길 권유합니다.
위의 내용을 정리하자면, 레이아웃 사이클은 1. 제약(Constraints), 2. 레이아웃, 3.그리기 의 순서로 스텝 1,2는 UIView와 UIViewController 양쪽에 메소드가 내장되어 있습니다.
Constraints
여기서 부터는 각각의 사이클에 대해 상세하게 설명해보고자 합니다. 우선은 제약(Constraints) 부분을 보도록 하겠습니다.
View의 updateConstraints
updateConstraints의 사용법
UIView의 updateConstrains는 제약(Constraints)을 갱신할때 호출되는 메소드 입니다. 서브클라스에서 오버라이드하여 사용합니다. 아래와 같이 적습니다. super를 가장 아래 호출하고 있는 것에 주의 해주십시오.
1 2 3 4 5 6 7 8 | override func updateConstraints() { // 제약을 갱신하는 코드를 여기에 적습니다. // 아직 레이아웃을 실행하는 setNeedsLayout, layoutIfNeeded는 호출하면 안됩니다. // setNeedsDisplay도 또한 호출하면 안됩니다. // super는 가장 마지막에 호출합니다. super.updateConstraints() } | cs |
updateConstraints의 호출방법
어플 내에서 콘텐츠가 변경된 경우, view의 제약(Constraints)을 갱신하고 싶을 때가 있을 겁니다. 이때, View의 updateConstrains를 코드로 직접 호출하면 안됩니다. 이것은 시스템이 적당한 타이밍에 호출해줍니다. 시스템에 제약(Constraints)의 갱신을 요청하기 위해 UIView에 setNeedsUpdateConstrains라는 메소드가 내장되어 있습니다.
setNeedsUpdateConstraints를 호출하는 순간은 제약(Constraints)의 갱신을 하지 않고 시스템이 한번에 정리하여 제약(Constraints)의 갱신을 실행하게 됩니다. 즉, 이라와 같은 setNeedsUpdateConstrains를 반복하여 호출해도 제약(Constraints)이 갱신되는것은 한번 뿐입니다. 이 메소드는 두의 updateConstraints를 호출하기 위해 표시해두는 의미로 사용합니다.
1 2 3 4 5 6 | func doSomething() { for _ in 0 ..< 10 { // 반복하여 호출하여도 updateConstraints는 한번만 호출됩니다. view.setNeedsUpdateConstraints() } } | cs |
또한 시스템이 정리해서 제약(Constraints)의 변경을 실행할 때 updateConstraintsIfNeeded를 호출합니다. 이 메소드는 개발자가 호출하는 것도 가능합니다. 이 메소드를 호출하기 전 setNeedsUpdateConstrains를 호출한 View만 updateConstrains가 호출 됩니다. 아래의 그림과 같이 view1과 3에서 setNeedsUpdateConstrains를 호출하여, view2에서 updateConstrainsIfNeeded를 호출한 경우 직후에 updateConstrains가 호출되는 것은 view3뿐입니다. view1은 view2의 Superview이므로 호출되지 안흣ㅂ니다. 이 경우 시스템이 view1의 updateConstraints를 호출합니다.
View의 Intrinsic Content Size란?
View는 Intrinsic Contents Size라고하는 독자의 사이즈를 갖고 있습니다. 사이즈 이므로 Horizontal과 Vertical 양방향으로 설정되어 있습니다. 한반향만 설정하는 것도 가능합니다. UILabel의 경우에는 텍스트를 딱 감쌀수 있는 사이즈가 Intrinsic Content Size 입니다. 또한 UIProgressView에서는 Vertical방향만 설정되어 있습니다. UIView에는 양축 모두 설정되어 있지 않습니다.
커스텀 뷰에서는 아래와 같이 오버라이드하여 설정가능합니다. 설정하지 않은 방향에서는 UIViewNoIntrinsicMetric을 설정합니다.
1 2 3 | override func intrinsicContentSize() -> CGSize { return CGSize(width: UIViewNoIntrinsicMetric, height: 10) } | cs |
Intrinsic Content Size를 설정하는 의미를 다음에 설명하겠습니다.
Content Hugging과 Compression resistance
Intrinsic Content Size가 정의 되어 있는 경우에는 Width와 Height를 결정하는 제약(Constraints)을 추가하지 않는 경우 Intrinsic Content Size의 사이즈에 리사이즈 됩니다. 이러한 이유는 Intrinsic Content Size가 설정되어 있는 경우에 디폴트로 아래의 제약(Constraints)이 추가되기 때문 입니다.
// UILabel의 Intrinsic Content Size = CGSize(width: 100, height: 30)의 경우
H:[label(<=100@250)]
H:[label(>=100@750)]]
V:[label(<=30@250)]
V:[label(>=30@750)]
Width와 Height를 경정하는 제약(Constraints)을 추가하지 않은 경우 위의 4개의 제약(Constraints)에 따라 라벨 사이즈는 w:100, h:30이 됩니다. 우의 표기는 제약(Constraints)의 Visual Format입니다. Visual Format은 다음과 같이 읽습니다.
Horizontal방향에 label의 폭이 100이하의 제약(Constraints). 우선순위(Priority)는 250
H:[label(<=100@250)]
Vertica방향에 label의 높이가 30이상의 제약(Constraints). 우선순위(Priority)는 750
V:[label(>=30@750)]
자세한 것은 Apple 문서에 적혀 있습니다.
위의 제약(Constraints)은 각각 이름이 붙어 있습니다.
H:[label(<=100@250)] <- Content Hugging
H:[label(>=100@750)] <- Compression Resistance
Content Hugging이 커지기 어려운 정도, compression resistance가 작아지기 어려운 정도를 나타냅니다. 각각의 프라이오리티가 다른 정도로 기타 제약(Constraints)에 따라 커질지 작아질지가 결정됩니다.
인터페이스 빌더(Interface Builder)나 코드에 설정하는 제약(Constraints)의 디폴트의 우선순위(priority)는 1000입니다. 그것 때문에 기본적으로 개발자 지신이 설정한 제약(Constraints)이 우선되리라 생각됩니다. Content Hugging과 Compression Resistance의 사용처는 UILabel이나 UIButton등 콘텐츠에 따라 동적으로 사이즈가 변할 때, 다시한번 넓이와 높이의 제약(Constraints)을 새롭게 결정하고 싶지 않을 때 사용합니다.
Content Hugging과 Compression Resistance는 InterfaceBuilder와 코드로 우선순위(priority)를 변경하는 것이 가능합니다.
view.setContentCompressionResistancePriority(UILayoutPriorityRequired, forAxis: UILayoutConstraintAxis.Horizontal)
Intrinsic Content Size를 동적으로 변경하기
Intrinsic Content Size를 참조하는 것은 View가 updateConstraints를 호출한 직후입니다. 그러나, 자동적으로 참조하는것은 처음 한번 뿐입니다. 거기서 커스텀뷰에서는 invalidateIntrinsicContentSize를 호출할 필요가 있습니다. 예를들어 UILabel은 text가 변경될 때 invalidateIntrinsicContentSize를 호출, Intrinsic Content Size를 재계산합니다.
커스텀뷰는 requiresConstraintBasedLayout를 사용
모든 제약(Constraints)을 updateConstraints안에 기술하고 있는 경우, 시스템은 updateConstraints를 호출하지 않습니다. AutoLayout에서 동작하는 커스텀뷰를 만들 때에는 UIView의 class func requiresConstraintBasedLayout() -> Bool을 오버라이드하는 것으로 해결합니다.
1 2 3 | override class func requiresConstraintBasedLayout() -> Bool { return true } | cs |
ViewControllerのupdateViewConstraints
UIViewController의 updateViewConstraints는 아래와 같이 적습니다.
1 2 3 4 5 6 | override updateViewConstraints() { super.updateViewConstraints() //여기에 self.view의 Subview의 제약을 갱신하는 코드를 적습니다. } | cs |
ViewController의 updateViewConstraints는 ViewController의 self.view의 updateConstraints의 대신입니다.
1 2 3 4 5 6 7 | class CustomViewController: UIViewController { func doSomthing() { self.view.setNeedsUpdateConstraints() self.view.updateConstraintsIfNeeded() // 여기서 self.updateViewConstraints()가 호출됩니다. } } | cs |
Layout
다음에 레이아웃 스텝입니다. 이 레이아웃 스텝시에는 제약이 확정되어 있고 그 제약(Constraints)을 바탕으로 뷰의 레이아웃을 실행합니다.
뷰의 layoutSubviews
layoutSubviews의 사용법
오버라이드시에는 super.layoutSubviews()를 반드시 호출합니다.
1 2 3 4 | override func layoutSubviews() { super.layoutSubviews() // 여기에 코드 } | cs |
주의 점으로는 이 메소드는 시스템을 호출하는 것으로 직접 호출하면 안됩니다. 이러한 개념은 제약(Constraints)과 같습니다.
layoutSubview의 호출 방법
이것도 setNeedsUpdateConstrains, updateConstrainsIfNeeded와 개념은 완전하게 같습니다. layoutIfNeeded를 호출한 View와 그 Subview중, setNeedsLayout을 호출한 View에 대하여 layoutSubview를 호출합니다.
NSLayoutConstraint의 값을 갱신한 경우
NSLayoutConstraint의 값을 갱신한 경우, 레이아웃은 자동적으로 조정되는 걸까요? 간단한 예로 검증해 봅시다. 그림과 같이 view1과 view2의 마진의 제약(Constraints)을 leftConstraint라고 합시다.
이 leftConstraint의 값을 변경한 경우, 제약(Constraints)이 관계하고 있는 View 중 부모의 View의 setNeedsLayout이 호출했을 때와 비슷한 움직임입니다. 상세한 것은 아래를 참고해 주세요.
1 2 3 4 5 6 7 | @IBOutlet weak var leftConstraint: NSLayoutConstraint! func doSomething() { leftConstraint.constant = 100 // 제약을 변경합니다. view2.layoutIfNeeded() // 아무것도 일어나지 않습니다. 일반적으로 view2의 layoutSubviews가 호출되어야 합니다. view1.layoutIfNeeded() // view1, view2의 layoutSubviews가 호출 됩니다 . } | cs |
즉, View1의 setNeedLayout이나 layoutIfNeeded를 적지 않아도 제약(Constraints)대로 레이아웃 됩니다.
ViewController의 viewWillLayoutSubviews와 viewDidLayoutSubviews
ViewController에 있는 viewWillLayoutSubviews, viewDidLayoutSubview. 이 두가지의 메소드는 self.view의 layoutSubviews의 직후에 호출됩니다. 샘플 소스는 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 | class CustomViewController: UIViewController { @IBOutlet weak var label: UILabel! // self.viewのSubviewです func doSomthing() { print("1") self.label.setNeedsLayout() self.view.setNeedsLayout() print("2") self.view.layoutIfNeeded() print("3") } } | cs |
1 2 3 4 5 6 7 8 | - Console - 1 2 ViewController: viewWillLayoutSubviews ViewController's view: layoutSubviews ViewController: viewDidLayoutSubviews label: layoutSubviews 3 | cs |
ViewController.view.Subview.layoutSubviews은 viewDidLayoutSubviews의 후에 호출되는 것에 주의 하십시오. 레이아웃은 Superview로부터 subview로 진행됩니다.
View의 레이아웃 사이클 메소드 정리
위에서 설명했던 View의 레이아웃 사이클의 메소드를 정리하겠습니다.
알기 쉽게 아래와 같이 명명했습니다.(정식 명칭이 아닙니다)
업데이트 메소드 : 기 메소드에서 값을 갱신합니다.
마크메소드 : 업데이트 메소드를 호출해야하는 View에 마크를 표시합니다.
트리거메소드 : 마크가 되어 잇는 View에 대해 업데이트 메소드를 각각호출 합니다.
사이클 | 업데이트 | 마크 | 트리거 |
---|---|---|---|
Constraints | updateConstraints | setNeedsUpdateConstraints | updateConstraintsIfNeeded |
Layout | layoutSubviews | setNeedsLayout | layoutIfNeeded |
Draw | drawRect | setNeedsDisplay, setNeedsDisplayInRect | 없음 |
본 포스트에서는 설명하지 않고 있지만 그리기 사이클에서는 setNeedDisplay와 setNeedsDisplayInRect를 다시그리기의 마크를 붙이기 위해 사용하였습니다.
AutoLayout과 애니메이션
AutoLayout에서 애니메이션을 사용하는 경우, 프레임을 조작하는 통상의 애니메이션과는 다른 방식으로 코딩을 합니다.
아래에 비교 샘플을 적어봤습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // frame을 변경하는 방법 UIView.animateWithDuration(1.0, animations: { () -> Void in view.frame.size.width = 100 }) // 제약을 변경하는 방법 // Constraint가 정의되어 추가 되있는 것으로 합니다. let widthConstraint: NSLayoutConstraint = ... view.addConstraint(widthConstraint) // 애니메이션전에 Constraint를 갱신 widthConstraint.constant = 100 // 레이아웃 실행 UIView.animateWithDuration(1.0, animations: { () -> Void in view.layoutIfNeeded() }) | cs |
AutoLayout에서 애니메이션을 실행하는 경우 먼저 제약(Constraints)을 갱신해 둡니다.
레이아웃 사이클의 3스텝을 생각해보면 알기쉬울거라 생각합니다. 레이아웃전에 스텝1의 제약(Constraints)이 완성되어 있을 필요가 있으므로, 애니메이션 블록의 전에 제약(Constraints)을 갱신하고 있습니다. 그리고 애니메이션의 구현부분은 프레임이 변경되어 있으므로 스텝2의 레이아웃에 해당합니다. 이미 레이아웃을 실행하기 원하므로 layoutIfNeeded를 사용합니다. 여기서 모든 기본은 제약(Constraints), 레이아웃, 그리기의 3스텝입니다.
코드로 AutoLayout을 구현
코드로 AutoLayout을 구현시 주의 점
코드로 AutoLayout을 구현할 경우, interfaceBuilder와는 다르게 UIView의 var translateAutoresizingMaskIntoConstraints:Bool을 false로 해 두어야 합니다. 이 프로퍼티는 디폴트로 true이며, true의 경우 View의 Autoresizing mask의 값으로부터 제약(Constraints)이 자동적으로 추가됩니다. 이 제약(Constraints)이 개발자 자신이 추가한 제약(Constraints)과 충돌을 일으켜 레이아웃이 이상해 질 경우도 있습니다.
Interface Builder에서 AutoLayout을 만들 때에는 시스템이 자동으로false로 설정되어 있으므로 코드에서 AutoLayout을 구현할 때에만 주의하여 주십시오.
Couples에서 사용하고 있는 Autolayout의 라이브러리 Cartography에 대하여
Couple에서는 현재 코드로 Autolayout을 구현하고 있습니다. 이 때 UIKit의 표준 API로는 제약(Constraints)을 하나 추가하는것만으로 엄청난 코드를 작성해야 합니다. 이것을 해소하기 위해 Cartography라는 라이브러리를 도입하고 있습니다. 이위에도 SnapKit또한 검토했지만 Cartography가 제일 Swift를 잘 활용하고 있어 채용하였습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 표준 API addConstraint(NSLayoutConstraint( item: button1, attribute: .Right, relatedBy: .Equal, toItem: button2, attribute: .Left, multiplier: 1.0, constant: -12.0 )) //CartoGraphy constrain(button1, button2) { button1, button2 in button1.right == button2.left - 12 } | cs |
게다가 위의 예에서는 좌측의 Button1의 translateAutoresizingMaskIntoConstraints를 false로 만들어 줍니다. 이 외에도 편리한 메소드가 내장되어 있으므로 상세는 Readme를 읽어주십시오.
끝으로
이번에는 iOS의 레이아웃 사이클을 중심으로 View의 레이아웃이 실행되는 방법을 정리해 보았습니다. 레이아웃에 관한 메소드는 많이 존재하므로 모든 것을 파악하기는 힘들다고 생각됩니다. 저도 처음에는 고생했어요.
iOS초심자분들은 이번에 소개한 제약(Constraints), 레이아웃, 그리기의 3스텝의 흐름과 setNeeds...등의 메소드의 사용방법을 이해해두시면 좋을 것이라 생각됩니다. 그 후에는 UIKit의 리퍼런스를 읽고 프로퍼티나 메소드를 사용하는 방법을 늘려간다면 점차 적응되리라 생각도비니다.
(참고로, 리퍼런스를 읽을 때에는 Dash라고하는 Mac의 어플리케이션이 큰 도움이 됩니다.)
이상 읽어주셔서 감사합니다.
Constraints 제약, 제한, 통제
Intrinsic 고유한, 본질적인
Hugging 껴안다, 바짝 붙어있다.
Compression 압축
resistance 저항