우선 main run loop를 알아야 합니다.
main run loop
기기에서 앱을 실행하면 @main을 찾아 UIApplecation객체와 AppDelegate객체를 생성합니다.
그리고 앱을 계속 실행하고 응답하도록 하는 loop(main run loop)를 실행합니다
발생한 모든 이벤트는 eventqueue에 추가되며, 이 loop는 발생하는 다양한 이벤트들을 처리하게 됩니다. (터치이벤트, 디바이스 회전등)
발생한 이벤트는 각 이벤트의 알맞는 핸들러를 찾아 그들에게 이벤트에 대한 처리권한을 위임하게됩니다.
이런 이벤트를 모두 처리하고 권한이 다시 main run loop로 돌아오는 시점을 "update cycle"이라고 한다.
Update Cycle
main run loop에서 이벤트가 처리되는 과정에서 버튼을 누르면 크기나 위치가 변화하는 애니메이션 같이 layout이나 position 값을 바꾸는 핸들러가 실행될 수 있습니다. 이런 변화는 즉각적으로 반영되는 것이 아닙니다. system은 layout, position이 변경되는 view를 확인합니다.
그리고 모든 핸들러가 종료되고 main run loop로 권한이 돌아오는 update cycle에서 view들의 값을 바꿔 position과 layout의 변화를 적용시킵니다.
그렇게 되면 Event 발생과 position, layout 변화간의 시간차가 존재할 것입니다. 이런 시간차의 존재를 인지해야 setNeedLayout을 이해할 수 있습니다.
layoutSubViews()
UIViewController의 viewWillAppear처럼 레이아웃이 결정되기 전, 후의 메서드가 존재합니다.
layouSubViews 역시 레이아웃이 결정되는 과정중에 레이아웃과 연관된 부가적인 일을 할 수 있도록 도와줍니다.
layoutSubViews는 UIView의 인스턴스 메서드입니다. 이 메서드가 호출되면 view와 subview들의 layoutsubview들이 연달아 호출됩니다.
그러므로 비용이 많이 들기 때문에 직접 호출하는 것은 지양해야 합니다.
ViewController에서 레이아웃이 결정되는 과정은 다음과 같습니다.
1. ViewController의 viewWillLayoutSubView() 메서드가 호출
2. ViewController의 contentView가 layoutSubViews()메서드 호출
- 레이아웃 정보를 바탕으로 레이아웃을 계산
- 이후 뷰 계층 구조를 순회하며 동일한 메서드를 호출해나감
- 변경된 정보를 바탕으로 변경 사항 반영
3. 변경된 view들이 viewDidLayoutSubViews()메서드 호출
layoutSubViews 관련 메서드
viewWillLayoutSubView
뷰의 bounds가 변경되면 뷰는 하위뷰들의 위치를 조정하는데, 레이아웃이 결정되기 전 여러 작업을 수행하고자 할 때 메서드를 override하여 사용합니다.
layoutSubViews()
뷰의 크기가 변경될 대 마다 이에 대응하여 하위 뷰 들의 크기, 위치가 변경되어야 합니다.
autoLayout을 사용하면 각 뷰의 autoresizeingMask프로퍼티를 설정하여 상위 뷰의 크기가 변경되었을 때 어떻게 대응할지 규칙을 정할 수 있습니다.
뷰의 크기 변경이 발생하면 하위 뷰들의 autoresizing 동작을 적용하는데, 변경 사항을 반영하기위해 layoutSubViews()메서드를 호출합니다. 그리고 하위 뷰들의 layoutSubViews들도 연쇄적으로 호출하게됩니다.
viewDidLayoutSubView()
레이아웃이 결정되고 나서 필요한 작업이 있을경우 override를 수행합니다.
- 다른 뷰의 값에 의존하는 행위
- 레이아웃이 변경된 후 데이터를 reload
layoutSubView가 호출되면 viewDidlayoutSubview가 호출됩니다. 갱신된 view 값에 의존하는 행위들은 이 메서드 내에 명시해주어야 합니다.
다시 돌아와서 layoutSubViews()를 보자면..
아래와 같은 상황에서 시스템이 자동으로 position이 변경되는 View를 체크하고 layoutSubView()들이 호출되게 됩니다.
- View의 크기를 조절할 때
- subView를 추가할 때
- UIScrollView를 스크롤할때
- 기기를 회전할 때
- View의 autolayout constraint를 변경할 때
위 시점에는 자동으로 updateCycle에서 layoutSubView()를 호출하는 행위를 예약합니다.
만약 수동으로 예약하려면? 여기서 setNeedsLayout()이 등장하게 됩니다.
setNeedsLayout()
- layoutSubViews()를 예약하는 행위중 가장 비용이 적게 드는 방법이 setNeedsLayout()을 호출하는 것입니다.
- 이 메서드를 호출한 view는 레이아웃이 재계산 되어야 하는 view라고 수동으로 체크가 되며 updatecycle에서 layoutSubViews()가 호출되게 됩니다.
- 이 메서드는 비동기적으로 작동하기 때문에 호출되고 바로 반환됩니다.
- 다음 updateCycle에 layoutSubViews()를 호출해달라고 "예약"하는 메서드입니다.
** 즉시 layout 갱신을 요청하는 메서드는 layoutIfNeeded()입니다.
다음으로 setNeedsDisplay()입니다.
setNeedsDisplay()
다음 UpdateCycle에 draw() 메서드를 호출하기를 "예약"합니다.
setNeedsDispaly()는 UIView의 인스턴스 메서드입니다.
view의 내용이 변경되어 view가 다시 그려질 필요가 있는 경우 호출됩니다.
여태 저는 호출한적이 없지만 View가 다시 그려진 이유는 UI와 직접적인 프로퍼티가 변경되면 이 setDeensDipaly함수가 저절로 호출된다고 합니다(backgroundColor, Content)등등..
예를 들어 아래처럼 움직이는 원을 보여주는 View가 존재할 때 View는 x, y, radius같은 값을 가지고 있을텐데,
class CircleView: UIView{
private var x: Int
private var y: Int
private var radius: Int
...
}
이 x, y, radius같은 값들은 UI와 관계가 직접적으로 없기 때문에 자동으로 setNeedsDispaly()가 호출되지 않습니다.
그러므로 아래처럼 직접 호출해주어야 합니다.
class CircleView: UIView {
private var x: Int = 0 {
didSet {
self.setNeedsDisplay()
}
}
private var y: Int = 0 {
didSet {
self.setNeedsDisplay()
}
}
private var radius: Int = 0 {
didSet {
self.setNeedsDisplay()
}
}
}
이렇게 setNeedsDisplay()를 호출하면 다음 UpdateCycle에 draw함수의 호출을 예약한다고 했습니다.
이 draw함수는 customView를 만들지 않느 이상 재정의할 필요가 없습니다.
** draw()메서드는 개발자가 직접 호출해선 안됩니다
override func draw(_ rect: CGRect) {
change(x: self.x, y: self.y)
change(radius: self.radius)
}
이렇게 draw를 재정의 하면 setNeedDispaly()로 예약한 UpdateCycle의 draw() 호출로 인해 뷰가 다시 그려지게 됩니다.
살짝 draw(_ rect: CGRect)를 살펴보자면
draw(_ rect: CGRect)
- 전달된 사각형(rect)내에서 수신자의 이미지를 그린다고합니다.
- 이 사각형은 view의 전체 범위이고
- 개발자가 직접 호출해선 안됩니다.
정리
setNeedsLayout과 setNeedsDispaly 두 메서드 모두 각각 layoutSubViews()와 draw()호출을 예약합니다.
호출된 메서드들은 다음 update cycle에 시스템이 호출하도록 예약이 됩니다.
update cycle은 main run loop가 이벤트를 각각의 대리자에게 처리하도록 권한을 넘기고, 이벤트가 처리된 후 권한이 main run loop에 돌아오면 updatecycle이 실행됩니다.
layoutSubViews()는 하위 뷰들의 레이아웃을 재조정합니다. 또한 draw()는 드로잉 사이클에 그려야할 컨텐츠들을 동시에 그리고 적용시킵니다.