안녕하세요, szzang입니다!!
굉장히 오랜만의 포스트입니다 😅
그동안 놀고있었던건 아니고… 나름 여러가지 공부를 하느라 헿..
그래도 오늘은 오랜만에 묵혀두었던 공부하고 싶었던 주제에 대한 공부를 해보고 나름대로 정리한 내용을 공유해보려 합니다!
바로 iOS의 Layout은 어떻게 업데이트 되는가..!!
출발해 보시죠! 😎
먼저 iOS Layout Update를 이해하기 위해서는 Run Loop에 대해 알아야 합니다.
그럼 이제 Run Loop를 알아보았으니, iOS의 Layout이 어떻게 업데이트 되는지 본격적으로 알아보겠습니다.
Automatic Layout Refresh Trigger
아래 동작들은 View가 자동적으로 Layout이 변화되었다는 flag를 변화시켜 다음 update cycle에서 layoutSubViews가 호출되게 됩니다.
자동으로 layoutSubViews()가 호출되게 하는 동작이외에도 직접 호출되게하는 방법도 존재합니다.
update cycle을 준수하기 때문에 가장 적은 부하로 layoutSubViews()를 호출할 수 있는 method입니다.
setNeedsLayout()은 즉시 View를 update 하지 않지만 다음 cycle에 layoutSubViews()를 호출하여 update 될 수 있도록 합니다.
마찬가지로 View의 Layout 시점과 View가 실제로 다시 그려지는 시점이 일치하지 않을 수 있습니다.
setNeedsLayout()과 달리 View가 즉시 update 되어야 한다면 즉시 layoutSubViews()를 호출합니다.
만약 자동적으로 flag를 변화시키는 동작이나 setNeedsLayout() 이후에 layoutIfNeeded()를 호출하게되면 그 즉시 layoutSubViews()를 호출합니다.
그러나 이때, Layout이 변화된 View가 없다면 layoutSubViews()는 호출되지 않습니다.
layoutIfNeeded()는 다음 update cycle을 기다릴 수 없는 상황에 유용합니다.
그렇지만 update cycle을 통해 run loop 한번 당 1번의 뷰 업데이트가 이루어지는것이 가장 이상적이라고 합니다.
또한 layoutIfNeeded()는 Constraint를 Animation 하는 상황에서 더욱 유용한데요,
Animation을 지정하고 closure안에 layoutIfNeeded()를 호출하면 Animation이 올바르게 동작하도록 할 수 있습니다.
Layout이 View의 크기와 위치를 의미했다면 Display는 View의 속성들 중 크기와 위치 그리고 Child View에 대한 정보를 제외한 모든 속성을 포함합니다.
예를들어 Color, Text, Image, Core Graphics가 있습니다.
Display는 Layout과 유사하게 System을 통해 업데이트를 하거나 직접 업데이트를 하는 method를 호출하는 방식이 있습니다.
UIView의 draw() method는 Layout 업데이트 과정에서 layoutSubViews와 같은 역할을 하는데,
크기와 위치를 제외한 모든 속성에 대한 표현을 진행합니다. 이때, Child View에 대한 정보를 가지고 있지 않으므로 subView의 draw는 호출하지 않습니다.
layoutSubViews()와 마찬가지로 직접 호출하는것 보단 System에 의해 호출되는 편이 부하가 적습니다.
Core Grahphics, UIKit을 활용하여 View에 Contents를 그릴때는 해당 method를 override해야 합니다.
View가 background Color 자체만 표시하거나 기본 레이어를 사용하는 경우에는 해당 method를 override 할 필요가 없습니다.
View에 Contents를 그릴때 UIGraphicsGetCurrentContext()를 사용하여 Graphic Context를 만들어 View에 표시할 수 있습니다.
이 때 Context가 draw() method 호출 간에 변경 될 수 있으므로 strong reference를 설정해서는 안됩니다.
setNeedsLayout()과 거의 유사하며 View의 Content가 업데이트 되어야하는 flag를 내부적으로 활성화 시키고 다음 update cycle에 draw()를 호출하여 View를 redraw합니다.
property의 변경에 따라 View를 다시그려야 하는 경우에는 property observer를 통해서 setNeedsDisplay()를 명시적으로 호출하도록 할 수 있습니다.
class SomeView: UIView {
var shape: String = "" {
didSet {
setNeedsDisplay()
}
}
}
override func draw(_ rect: CGRect) {
if shape == "rect" {
drawRect()
} else if shape == "triangle" {
drawTriangle()
} else if shape == "circle" {
drawCircle()
}
return
}
위 코드에서는 draw()가 method override를 하고 있지만, super를 따로 호출하지 않았습니다.
UIView를 직접 서브클래스하는 경우에는 draw()는 super를 호출할 필요가 없습니다. 그러나, 다른 View 클래스 를 서브클래스하는 경우에는 super를 호출해야 합니다.
AutoLayout에는 아래와 같은 과정이 필요합니다.
Auto Layout을 사용하는 View의 Constraint를 동적으로 변경합니다.
layout의 layoutSubViews(), display의 draw()처럼 직접 호출해서는 안됩니다.
updateConstraints()에서는 일반적으로 동적으로 변하는 Constraint들을 구현하고, 정적인 Constraint들은 View의 생성자나 viewDidLoad()에 작성합니다.
Constraint를 활성/비활성화 하거나 priority 변경, constant 수정, View를 hierarchy에서 제거하는것은 System이 다음 update cycle에서 updateConstraints()를 호출하게합니다.
Layout, Display와 마찬가지로 명시적으로 호출하는 방법도 존재합니다.
다음 update cycle에서 updateConstraints를 호출하게 하며, setNeedsLayout(), setNeesdDisplay()와 비슷하게 작동합니다.
View의 property가 Constraint에 영향을 미친다고 가정하고, 해당 property에 변경이 생긴다면 명시적으로 setNeedsUpdatedConstraints()를 호출하여 다음 update cycle에 updateConstraints()가 호출되게 할 수 있습니다.
이러한 방식은 여러 Constraint가 업데이트 될 때마다 영향을 받는 Constraint들이 다음 updateConstraints()에서 한번에 계산할 수 있도록 하여 최적화를 제공할 수 있습니다.
Layout의 layoutIfNeeded()와 유사하며 AutoLayout을 사용하는 View에게만 유효합니다.
Constriant Update Flag가 변경되면 즉시 updateConstraints()를 호출하며 flag는 자동으로 설정되거나 setNeedsUpdateConstraints(), invalidateIntrinsicContentSize를 통해 변경될 수 있습니다.
invalidateIntrinsicContentSize()
Auto Layout을 사용하는 일부 View들은 intrinsicContentSize를 가집니다. (UIButton이나 UILabel 등)
이때 intrinsicContentSize는 뷰 자체가 가지는 속성만을 고려한 크기입니다. constraint에 의해 늘어난 영역들은 제외됩니다.
위 내용에서 미루어보아 invalidateIntrinsicContentSize는 intrinsicContentSize가 다시 계산되어야 함을 의미하며 Constraint Update Flag를 갱신하여 다음 update cycle에 updateConstraint를 호출하게 됩니다.
앞서 살펴본 Layout, Display, Constraint는 run loop에서 setNeedsLayout(), setNeedsDisplay(), setNeedUpdateConstraint()를 통해 update cycle에 호출될 layoutSubViews, draw, updateConstraint를 시스템이 호출하는 방식으로 비슷하게 동작합니다.
또한 다음 update cycle 이전에 즉시 업데이트 되어야 한다면 명시적으로 호출하는 방법도 가지고 있습니다.
모든 내용을 종합해보면 다음과 같은 도표로 정리할 수 있습니다.
View가 init될 때는 Constraint, Layout, Display 순으로 호출되게 됩니다.
오늘은 그동안 더 깊게 공부해보고 싶었엇던 “Layout Update는 어떻게 이루어지는지” 에 대해 공부하고 정리해보았습니다.
이렇게 정리한다고 꼭 다 제 지식이 되는것은 아니지만 ..
정말 안좋은 기억력을 가진 저이지만
머릿속에 조금은 더 남게되니 역시 공부를하고 정리를 하는건 좋은것 같습니다 🙂
곧 Mash-Up 프로젝트에도 또 매진할 것 같습니다.
(이번엔 오랜만에 Daily Issue를 만들만한 소재가 있을지도 ?! 🤩)
그러면 다음포스트에서 뵙겠습니다
읽어주셔서 감사합니다. 🙇🏻
참고 :
https://medium.com/mj-studio/번역-ios-레이아웃의-미스터리를-파헤치다-2cfa99e942f9