우리가 처음으로 iOS 애플리케이션을 만들 때, 어쩔 수 없이 맞닥뜨리는 문제는 View의 Layout과 Content를 다루는 것입니다. 종종, 이 문제들은 우리가 실제로 UIView
가 언제 Update 되는지 잘못 이해하고 있기 때문에 발생합니다. 뷰가 언제 Update 되는지 정확하게 이해하기 위해서는 iOS 애플리케이션의 run loop에 대한 깊은 이해와 그것이 UIView
가 제공하는 몇몇의 메서드들과 어떤 관계성을 띄고 있는지 파악하는 것이 중요합니다. 이 포스팅은 이러한 상호작용들과 우리가 원하는 방식으로 뷰를 작동하게 만들기 위해 UIView
의 메서드들을 어떻게 사용하여야 할지 다룹니다.
iOS 앱의 Main run loop
iOS 애플리케이션의 main run loop는 유저로부터의 모든 input 이벤트를 받고, 적절한 응답을 해주는 것을 담당합니다. 유저가 발생시킨 모든 상호작용은 event queue에 추가됩니다. 아래의 사진에서 보이는 애플리케이션 객체(Application object)는 event queue로부터 이벤트를 하나씩 꺼내서 애플리케이션의 다른 객체들에게 전달합니다. 본질적으로, 애플리케이션 객체는 유저로부터의 input 이벤트를 해석하고 그에 상응되는 애플리케이션의 Core object들 안에 있는 핸들러를 호출해줍니다. 이러한 핸들러들은 우리, 개발자들이 쓴 코드를 호출해줍니다. 이러한 메서드들이 반환되면 다시 컨트롤은 main run loop로 돌아가서 Update Cycle이 다시 시작됩니다. Update Cycle은 View 들을 배치하고 다시 그리는 역할을 합니다(다음 섹션에서 설명합니다). 다음은 애플리케이션이 기기와 유저로부터의 input과 상호작용하는지 설명하는 그림입니다.
Update Cycle
Update Cycle
은 애플리케이션이 유저로부터의 모든 이벤트 핸들링 코드를 수행하고 다시 main run loop
로 컨트롤을 반환하는 지점입니다. 바로 이 지점에서 시스템은 우리의 View들을 배치하고(layout), 보여주고(display) 제약합니다(constraints). 만약 우리가 이벤트 핸들러들을 처리하는 과정에서 어떤 UIView
에 대해 변화를 준다면, 이 UIView
는 다시 그려져야(redraw) 한다고 표시됩니다. 다음 Update Cycle
에서, 시스템은 이 UIView
의 모든 변화를 수행합니다. 유저가 상호작용하는 것과 레이아웃이 변하는 시간의 갭은 유저가 인지하지 못할 정도여야 합니다. iOS 애플리케이션은 초당 60프레임을 보여주기 때문에, 한 번의 Update Cycle
은 1/60초 밖에 안 걸립니다. 이렇게 빠르게 업데이트가 되기 때문에, 유저는 UI와 상호작용 간의 차이를 느끼지 못합니다. 그러나, 이벤트가 처리되는 시점과 실제로 View가 다시 그려지는 시점에 차이가 있기 때문에, View는 우리가 View를 업데이트 하기를 원하는 run loop의 특정 시점에 업데이트가 되지 않을 수 있습니다. 이는 다음과 같은 위험을 초래합니다. 만약 우리가 View의 마지막 Layout이나 Content에 대해 계산을 해야 하는 시점이라면, 예전 정보를 갖고 View를 조작할 수 있는 가능성이 생깁니다. run loop
와 update cycle
그리고 몇 개의 UIView
의 메서드들을 이해하는 것은 위와 같은 문제들을 쉽게 피하고 원인을 분석하게 도와줄 것입니다.
다음은 main run loop
가 한 바퀴 돌 때, Update Cycle
이 언제 발생하는지를 표현한 그림입니다.
Layout
UIView
의 Layout
이란 것은, 화면에서 UIView
의 크기와 위치를 의미합니다. 모든 View
는 frame을 갖고 있고, 이는 부모 뷰의 Coordinate System(좌표계)에서 어디에 위치하고 얼마나 크기를 차지하는지를 나타냅니다. UIView
는 시스템에게 UIView
의 레이아웃이 변했다고 알려줄 수 있는 메서드나, View
의 레이아웃이 다시 계산되는 시점에 특정한 작업을 실행할 수 있게 오버라이딩할 수 있는 콜백 메서드도 제공합니다.
layoutSubviews()
layoutSubviews
라는 UIView
의 메서드는 View
와 자식 View
들의 위치와 크기를 재조정합니다. 이는 현재 뷰와 모든 자식 뷰의 위치와 크기를 제공합니다. 이 메서드는 재귀적으로 모든 자식 뷰의 layoutSubviews
까지 호출해야 하기 때문에 실행 시에 부하가 큰 메서드입니다. 시스템은 layoutSubviews
를 뷰의 frame
을 다시 계산해야 할 때 호출하기 때문에 우리는 layoutSubviews
를 오버라이딩해서 frame
이나 특정한 위치와 크기를 조절할 수 있습니다. 그러나 레이아웃을 업데이트해야 할 때 layoutSubviews
를 직접 호출하는 것은 금지되어 있습니다. 대신에, layoutSubviews
를 시스템이 호출하도록 유도할 수 있는 여러 개의 방식들이 존재합니다. 이러한 방식들은 모두 run loop
가 돌아가는 동안 layoutSubviews
가 실행되는 시점이 다르며, 직접 layoutSubviews
를 호출하는 것보다는 부하가 덜합니다.
layoutSubviews
가 완료될 때, viewDidLayoutSubviews
가 View
를 소유한 ViewController
에서 호출됩니다. layoutSubviews
는 View
의 layout
이 변화했다는 유일한 콜백이기 때문에, 우리는 레이아웃의 크기나 위치와 연관된 로직을 viewDidLoad
나 viewDidAppear
가 아닌, viewDidLayoutSubviews
에 호출해야 합니다. 이것이 오래된 레이아웃이나 위치 변수를 다른 계산에 사용하는 실수를 막는 유일한 방법입니다.
Automatic refresh triggers
다음과 같은 이벤트들은 자동으로 View
가 그것들의 layout
에 변화가 생겼다고 표시를 해주어서 개발자가 직접 요청할 필요 없이, layoutSubviews
가 다음 기회에 호출이 되게 해줍니다.
View
를 ResizingSubView
를 추가UIScrollView
를 스크롤할 때,UIScrollView
와 그것의 부모뷰에layoutSubviews
가 호출- Device를 회전(orientation change)
View
의Constraint
를 변경
위의 방법들은 자동으로 시스템이 View
의 위치가 변했고, 다시 계산되도록 하여 결국엔 layoutSubviews
가 호출되게 해줍니다. 그러나 layoutSubviews
를 직접 호출해줄 수 있는 방법들도 존재합니다.
setNeedsLayout
layoutSubviews
를 가장 적은 부하로 호출할 수 있는 메서드는 UIView
의 setNeedsLayout
메서드입니다. setNeedsLayouts
은 시스템에게 이 View
의 layout
이 다시 계산되어야 한다고 알려줍니다. setNeedsLayout
은 즉시 반환되고, 실제로 View
를 업데이트해주는 것은 아닙니다. 대신에, 시스템이 다음 Update Cycle
에서 layoutSubviews를 View
와 자식 View
들에게 호출하게 하고 그 시점에 setNeedsLayout
이 호출된 부들은 Update Cycle
에서 업데이트가 되도록 해줍니다. 이는 시각적으로 아무런 영향을 끼치지 않습니다. setNeedsLayout
이 호출되는 시점과 View
가 다시 그려지는 시점이 정확히 일치하지는 않지만, 그걸 유저가 인지할 수 없을 만큼 짧은 시간이기 때문입니다.
layoutIfNeeded
layoutIfNeeded
는 UIView
가 layoutSubviews
를 호출하도록 하는 또 다른 명시적인 메서드입니다. layoutSubviews
가 다음 Update Cycle
에서 호출되도록 하는 것이 아니라 layoutIfNeeded
는 만약 View
가 layout
이 재조정되어야 한다면, 즉시 layoutSubviews
를 호출해버립니다. 만약 우리가 layoutIfNeeded
를 setNeedsLayout
를 호출한 직후나 자동으로 layoutSubviews
를 호출해주는 방법들 직후에 호출했다면, layoutSubviews
는 뷰에 즉시 호출됩니다. 그러나 우리가 layoutIfNeeded
를 호출했는데 View
가 재조정되어야 하는 이유가 없다면, layoutSubviews
는 호출되지 않습니다. 만약 우리가 동일한 run loop
에서 레이아웃의 업데이트 없이 layoutIfNeeded
를 두 번 호출했다면, 두 번째 호출은 layoutSubviews
를 발생시키지 않습니다.
layoutIfNeeded
를 사용한다면, 레이아웃을 하는 것과 자식 View
들을 다시 그리는 것은 즉시 실행되고 이 메서드가 반환되기 전에 실행됩니다(애니메이션 상황을 제외하고). setNeedsLayout
과는 다르게, 이 메서드는 다음 Update Cycle
까지 뷰의 변화를 기다릴 수 없는 상황에 유용합니다. 그러나, 이러한 상황이 아니라면 그냥 setNeedsLayout
을 호출해서 다음 Update Cycle
에 뷰가 업데이트되어 run loop
한번 당 View업데이트가 한 번만 이루어지게 하는 것이 이상적입니다.
layoutIfNeeded
는 Constraints
를 애니메이션 하는 상황에서 특히 유용합니다. 우리는 애니메이션이 시작하기 전에 layoutIfNeeded
를 호출해서 모든 레이아웃 업데이트가 애니메이션 전에 수행되도록 전파할 수 있습니다. 새로운 Constraints
를 설정하고, 애니메이션 클로저 안에서는 또 layoutIfNeeded
를 호출해서 애니메이션이 올바른 상태로 진행되도록 할 수 있습니다.
Display
Layout
이란 것이 뷰의 위치와 크기를 의미한다면, Display
는 뷰의 속성들 중 크기와 위치나 뷰의 자식 View
들에 대한 정보를 갖지 않는 속성들을 포함합니다. 예를 들어, 색, 텍스트, 이미지, Core Graphics
그리기 등이 있습니다. Display
는 Layout
과정과 유사한데, 시스템이 자동으로 업데이트가 되게 하는 방식과 우리가 명시적으로 업데이트를 해주게 하는 방식(메서드들)이 존재합니다.
draw(_:)
UIView
의 draw
메서드는 Layout
업데이트 과정에서의 layoutSubviews
와 같은 역할을 합니다. 그러나 큰 차이점은 draw
메서드는 자식 View
들의 draw
까지 호출해주지는 않는다는 점입니다. layoutSubviews
와 마찬가지로 draw
를 직접 사용하는 것은 좋지 않습니다.
setNeedsDisplay()
이 메서드는 setNeedsLayout
과 유사합니다. View
의 Content가 업데이트 되게 하는 내부 플래그를 활성화시키고 실제로 View
가 다시 그리기 전에 메서드는 반환합니다. 그러면, 다음 Update Cycle
에 시스템은 이 플래그가 활성화되어있는 View
들을 draw
를 호출해서 다시 그려줍니다. 우리가 만약 View
의 일부분만 다시 그려지길 원한다면, setNeedsDisplay 메서드의 인자로 rect
를 전달할 수 있습니다.
대부분, 뷰의 UI 컴포넌트를 업데이트하는 것은 View
의 dirty flag
를 활성화시켜서 우리가 명시적으로 setNeedsDisplay
를 호출하지 않아도 다음 Update Cycle
에 뷰가 다시 그려지도록 유도합니다. 그러나, 만약 UI 컴포넌트와 직접적으로 연관되어 있지 않지만 매 Update Cycle
마다 다시 뷰를 그려주어야 하는 속성이 있다면 우린 didSet
속성 감시자를 설정하고 setNeedsDisplay
를 명시적으로 호출해줄 수 있습니다.
때때로 속성을 설정하는 것이 커스텀 하게 View
그리기를 요구할 수 있습니다. 다음과 같은 예시를 봅시다.
class MyView: UIView {
var numberOfPoints = 0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
switch numberOfPoints {
case 0:
return
case 1:
drawPoint(rect)
case 2:
drawLine(rect)
case 3:
drawTriangle(rect)
case 4:
drawRectangle(rect)
case 5:
drawPentagon(rect)
default:
drawEllipse(rect)
}
}
}
numberOfPoints
가 변하면 draw(_:)
안에서 View
를 그리는 방식이 달라지기 때문에 didSet
블록 안에 setNeedsDisplay
를 명시적으로 호출해준 예시입니다.
Layout
과정과 다르게 Display
는 즉시 draw(_:)
를 호출해주는 메서드는 존재하지 않습니다. 이는 뷰가 다시 그려지기 위해 다음 Update Cycle
을 기다리는 것이 아무런 문제가 없기 때문입니다.
Constraints
Auto Layout의 세계에서는 Layout
하고 Draw
하는것에 대해 3단계의 과정이 있습니다.
- Constraints를 업데이트한다 : 시스템이
View
에 필요한Constraints
들을 계산하고 설정한다. - Layout 단계 : 레이아웃 엔진이
View
들의Frame
과 자식View
들의Frame
을 계산하고 배치한다. - Display 단계 : View
의
컨텐츠를 다시 그리고 필요하다면 draw 메소드를 호출한다.
updateConstraints
이 메서드는 Auto Layout을 이용하는 View
의 Constraints
를 동적으로 변경할 때 사용될 수 있습니다. Layout
단계에서 layoutSubviews
나 Display
단계에서 draw
같이, updateConstraints
는 오직 오버라이딩되어야 하며 명시적으로 호출되어서는 안됩니다. 우리는 보통 updateConstraints
에서 동적으로 변해야 하는 Constraints
들을 구현합니다. 정적인 Constraints
들은 Interface Builder나 View
의 생성자나 viewDidLoad
에서 정의되어야 합니다.
일반적으로, Constraints
를 활성화/비활성화하거나 Constraints
의 우선순위나 constant를 변경하거나 View
를 View
계층에서 삭제하는 것은 updateConstraints
를 다음 Update Cycle
에서 호출하게 합니다. 그러나, UpdateConstraints
를 명시적으로 호출하는 방법 또한 존재합니다.
setNeedsUpdateConstraints
setNeedsUpdateConstraints
를 호출하는 것은 다음 Update Cycle
에서 Constraint
가 업데이트되는 것을 보장해줍니다. 이 메서드는 setNeedsLayout
이나 setNeedsDisplay
와 비슷하게 작동합니다.
updateConstraintsIfNeeded
이 메서드는 layoutIfNeeded
와 유사합니다. 그러나 오직 Auto Layout을 사용하는 뷰에만 유효합니다. 이 메서드는 Constraint Update Flag
(이 Flag는 자동으로 설정되거나, setNeedsUpdateConstraints
를 통해 설정되거나, invalidateIntrinsicContentSize
를 통해 설정될 수 있습니다.)를 검사합니다. 만약 Constraints
가 업데이트가 되어야 하면, updateConstraints
를 즉시 호출합니다.
invalidateIntrinsicContentSize
Auto Layout을 사용하는 몇몇 View
들은 intrinsicContentSize
속성을 갖습니다. 이는 View가 갖고 있는 Content의 크기입니다. intrinsicContentSize
는 전형적으로 View가 갖고 있는 요소들의 Constraints
으로 결정되지만, 이것 또한 커스텀 한 동작을 오버라이딩하여 제공할 수 있습니다. invalidateIntrinsicContentSize
를 호출하는 것은 View
가 갖고 있는 intrinsicContentSize
가 낡았으며, 다음 Update Cycle
에서 다시 계산되어야 한다고 플래그를 활성화시켜줍니다.
How it all connects
View
의 Layout
과 Display
그리고 Constraints
는 run loop
에서 다른 시점에 어떻게 업데이트되고, 명시적으로 업데이트할 수 있는지에 대해 유사한 패턴을 갖습니다. 각 컴포넌트들은 layoutSubviews
, draw
, updateConstraints
과 같은 실제로 업데이트를 전파하는 메서드들을 가지며, 명시적으로 호출되면 안 되기 때문에 이를 호출하도록 유도할 수 있는 방법들이 있습니다. 이러한 메서드들은 run loop
의 마지막에 View
의 해당 flag
가 활성화되어있으면 시스템이 호출해주는 방식입니다. 몇 가지의 자동적으로 이 Flag
들을 활성화해주는 방식들이 있고 명시적으로 활성화시켜주는 방식도 있습니다. Layout
과 Constraints
에 관련된 업데이트들에 대해서는 만약 다음 Update Cycle
까지 기다릴 수 없다면, 즉시 업데이트가 되도록 요청하는 메서드들도 존재합니다. 다음과 같은 표는 이러한 각 메서드들이 작동하는 방식입니다.
다음과 같은 표는 Update Cycle
과 Event Loop
그리고 위에서 설명한 메서드들이 Cycle
동안 어떻게 호출되는지 설명합니다. 우리는 명시적으로 layoutIfNeeded
나 updateConstraintsIfNeeded
를 run loop
의 아무 시점에서나 호출할 수 있습니다. loop의 끝은 Update Cycle
입니다. Update Cycle
은 Constraints
, Layout
그리고 Display
를 해당 플래그가 활성화되어있다면 업데이트해줍니다. 이러한 업데이트들이 완료되면, run loop
는 다시 시작합니다.
이것은 차트와 표를 요약하고 위에서 설명한 메서드들을 더욱 세분화하며 이 메서드들이 iOS의 run loop
와 작동하는 방식을 확실하게 설명해줍니다. 이 메서드들을 이해하고 어떻게 효율적으로 적절한 업데이트를 호출시키는 것은 낡은 레이아웃이나 콘텐츠를 이용하거나 예상치 못한 행동으로부터 초래하는 문제들을 쉽게 피할 수 있게 해줄 것입니다.
- Github
- Website
- Medium Blog, Dev Blog, Naver Blog
- Contact: mym0404@gmail.com