혼자 그냥 멍때리는데 View를 그리며 문제 생겼던 부분들이 어떤 것이 있었을까 하고 고민하다가 그냥 심심해서 구글링하며 커스텀 뷰 이쁘장하게 되있는건 어째 구현했을랑가싶어서 구경 좀 했다.
근데 생각해보니 이쁘장한 커스텀뷰를 만들 때 뷰가 어떻게 그려지는 지에 대해 대충은 알고 있는데, 좀 깊이 살펴보며 포스팅한 것은 없는 것 같아서 공부할 겸 씀
우선 뷰가 어떻게 그려지고.. 어떻게 업데이트되며.. 레이아웃을 어떻게 잡는지에 대해서 이해하려면 Main RunLoop에 대해서 이해해야 한다고 한다.
ㅇㅇ 그런가보다 하고 공부하믄되것지 뭐~~
Main RunLoop
별개로 우선 알아야 할 것은 내가 밑에서 설명할 RunLoop는 Main Thread에서 돌고있는 놈이다. 얘는 자동으로 돌고 있지만, 다른 global, custom Thread에선 자동으로 돌고있지않아 수동으로 처리해줘야한다.
RunLoop는 사용자로부터 발생한 이벤트뿐만이 아닌 Timer의 이벤트도 처리하는데, Main Thread에서 가진 RunLoop가 아닌 다른 Thread의 RunLoop는 자동으로 돌고있지않기 때문에 사용자로부터의 Input이나 Timer의 Interrupt event를 처리하지 못함을 알고 넘어가자.
자 일단 그림만 보며 간단히 흐름을 파악해보자
사용자로부터 이벤트가 발생하면 우선 OS에게 이벤트가 당연히 갈 것이다. 그림만 봐도 그렇잖음
그리고 OS에서 RunLoop로 이벤트를 직빵으로 보내는 것이 아닌 Event Queue에 담아놓는다.
Queue로 이뤄진 대기열이니 First In First Out로 동작하며 RunLoop가 돌 때 해당 대기열에서 이벤트를 가져온다.
가져온 이벤트를 Application Object에 전달하고 Application Object는 Core Object의 Handler를 호출한다.
그럼 Handler는 개발자가 작성한 코드 등, 각 이벤트에 대한 처리기를 실행한다.
RunLoop는 자신의 Cycle에 실행했던 코드들이 모두 반환되면 Update Cycle이라는 것을 실행한다.
Update Cycle은 레이아웃 관련 코드(layoutIfNeeded 등등)을 공부해봤다면 한번쯤 들어봤을 것이다.
Update Cycle
Update Cycle은 위의 Main RunLoop에서 설명했듯 RunLoop에 실행했던 함수들이 모두 반환된 후 RunLoop의 가장 마지막에 실행된다.
RunLoop에서 발생한 이벤트들을 처리하는 동안 화면을 업데이트해야하는 일이 생기면 "얘 업데이트 해야해" 라고 예약을 걸어놓고, Update Cycle 시점에 화면에 대한 변경사항을 처리한다.
다른 시점으로 보면 내가 화면에 대한 변화! 를 요청했어도 바로 업데이트가 되지않고, Update Cycle이 와야 업데이트가 된다.
즉, Update Cycle은 화면을 업데이트하는 시점이다.
별다른 처리없이 화면을 업데이트하도록 하였는데, 난 바로바로 째깍재깍 업데이트 되던디? 라는 생각이 든다면.. 뭐 그게 맞을 수도 있지만 화면에 대한 계산이 간단하여 딜레이가 크게 발생하지 않아서 차이를 못느꼈다던지.. 그런 것이겠지?
아이폰의 화면은 60Hz를 지원하기 때문에 1초에 60번 업데이트 사이클을 실행하기 때문에 딜레이를 크게 못느낀 것도 있을 것이다.
화면을 업데이트? ㅇㅇ
그래~~ Layout, Display, Constraint를 업데이트하는 시점이다~!!!
근데.. 방금 말했듯이 요청을 해도 바로 업데이트를 해주지 않는다.
아니 난 바로 업데이트 해줬음 좋겠는데 해달란대로 안해주면 짜잉나잖아요
그래서 UIView는 개발자들이 Update Cycle을 제어할 수 있게끔 여러 메서드들을 제공한다.
뭐.. 바로 업데이트해줘!!!, 얘는 좀 이따 해도돼!!, 얘는 이거 업데이트 해줘!! 등등 있겠지?
그럼 먼저 Layout 업데이트 관련 메서드들을 알아보자
Layout
Layout은 특정 뷰의 위치 좌표, 크기를 말한다.
위치 좌표, 크기하니깐 생각나는거 있지않음????
요 놈이 있쥐
요 놈을 업데이트해줄 때와 관련된 메서드들은 뭐가 있을까?
layoutSubviews()
? 뭐요
그냥 subviews를 배치한단다.
좀 더 자세히 알아보자면 이 메서드를 호출한 뷰와 하위 뷰들의 위치와 크기를 업데이트해주는 메서드이다.
근데 난 얘를 써먹은 적이 없는데 뷰들의 위치가 무슨 수로 잡혔던거임? 한다면
시스템에서 특정 조건이 성립된 뷰를 이래 저래 업데이트해라 하고 예약을 걸어놓는다.
그럼 Update Cycle에 layoutSubviews()를 호출하여 업데이트가 예약되어있는 뷰들을 업데이트한다.
특정 조건은 아래와 같다.
- 뷰의 크기가 변경되었을 때
- 하위 뷰가 추가되었을 때
- 디바이스가 회전되었을 때
- 뷰의 Constraints를 변경하였을 때
근데 이 메서드는 메서드를 호출한 뷰와 그 뷰의 하위 뷰까지 싸그리 업데이트를 하므로 비용이 매우 크다.
그래서 애플은 이 놈을 직접 호출하지 말아라!!!! 라고 한다.
이것이 뭔 말이냐..?
layoutSubviews()는 Update Cycle 시점에만 알아서 돌게끔 놔둬!!!! 넌 그냥 Update Cycle에 어떤 뷰를 업데이트 할 것인지 명시적으로 예약을 걸거나, Update Cycle 시점을 강제로 당겨와서 작업을 해!!! 쟤를 어거지로 호출하지말고 내가 제공하는 메서드를 써!!! 어떤 작업을 할 것인지 정해놓고 싶다면 Override만 해라..
최선을 다해서 설명한거임 이해가 됐길 바람
그럼 이제 layoutSubviews를 직접 호출하는 대신에 사용해야할 애플에서 제공한 메서드를 알아보자
setNeedsLayout()
NEXT UPDATE CYCLE!!! NEXT UPDATE CYCLE!!! NEXT UPDATE CYCLE!!!
setNeedsLayout()는 위에서 말한 것 중 "넌 그냥 Update Cycle에 어떤 뷰를 업데이트 할 것인지 명시적으로 예약을 걸거나" 에 해당한다.
해당 메서드를 호출하여 Update Cycle에 이 뷰 업데이트해줘! 하고 예약을 건 뒤 바로 반환된다. 비동기적으로 동작한다는 소리다.
그럼 예약을 걸어놨으니 다음 Update Cycle에 layoutSubviews가 동작하며 예약된 뷰의 레이아웃을 업데이트한다.
업데이트 해야할 뷰들을 Update Cycle에 한꺼번에 업데이트하므로 성능적으로 우세하다.
근데.. 나는.. 버튼을 누르면 뷰가 막 순간이동해서 파파팍 애니메이션있고 그러고싶은데.. 바로바로 업데이트 안되고 지연이 조금이라도 생겨버리면 별로일 것 같은디..?
나는 다음 Update Cycle까지 기다리기 싫어~~ 할 때 사용할 수 있는 메서드가 있다.
layoutIfNeeded()
immediately!! immediately!! immediately!!
layoutIfNeeded가 실행되면 다음 Update Cycle까지 기다리지 않고, Update Cycle을 바로 땡겨온다.
이 메서드는 뷰의 업데이트를 곧장 실행해 업데이트가 완료되면 메서드가 반환되는 동기 메서드이다.
"Update Cycle 시점을 강제로 당겨와서 작업을 해!!!" 이 말은 즉슨, layoutSubviews()를 바로 호출해라! 라는 것이니 역시나 비용이 비싸다.
그래서 애플은 꼭 필요할 때만 쓰고 아니면 setNeedsLayout을 사용하라고 권장한다.
layoutIfNeeded는 화면이 즉각적으로 업데이트되야하는 애니메이션 작업에 많이 사용된다.
그러니 애니메이션을 예로 들어 setNeedsLayout와 layoutIfNeeded를 비교해보자
부드러운 것이 layoutIfNeeded, 뚝딱거리는 것이 setNeedsLayout이다.
setNeedsLayout은 업데이트할 것들을 Update Cycle에 명시해놓고 다음 Cycle에 한꺼번에 업데이트하기 때문에 뚝뚝 끊기는 모습이다.
하지만 layoutIfNeeded는 뷰가 업데이트되는 것을 동기적으로 즉각 업데이트가 되기때문에 부드럽게 올라오는 모습을 보인다.
이해가 가지 않는다면 UIView의 animate 메서드를 생각해보자
@IBAction func tap(_ sender: UIButton) {
self.topConstraint.constant = 100
print(1)
UIView.animate(withDuration: 3) {
print(2)
self.view.layoutIfNeeded()
print(3)
}
}
순서대로 쭉 훑으며 과정을 살펴보자
- 뷰의 topConstraint를 변경하였다. Constraint는 변경이 되었지만, 아직 화면은 변화가 없다. Cycle이 안왔으니깐
- 애니메이션 블록 내부에서 layoutIfNeeded 메서드를 호출하였고, 1번에서 뷰의 Constraint가 업데이트되었다.
- 애니메이션 메서드는 Constraint를 변경하기 전의 View Constraint값과 변경한 후의 View Constraint값을 얻었고, 이를 3초라는 시간 동안 변경 전 값에서 변경 후 값으로 업데이트한다.
만일 layoutIfNeed가 없다면?
@IBAction func tap(_ sender: UIButton) {
self.topConstraint.constant = 100
}
뷰의 constraint값을 변경하였고, 이는 setNeedsLayout 메서드를 트리거하여 Update Cycle 시점에 한꺼번에 변경된다.
Q: 어쨋든 변경되는거고 만일 위 코드에 애니메이션 블록이 있다면 View Constraint 변경 전과 View Constraint 변경 후 값이 있으니 이것도 부드럽게 업데이트되야 되는거 아님?
A: ㄴㄴ, layoutIfNeeded를 호출하지않았으므로 뷰는 아직 업데이트 되지 않았음. 애니메이션 블럭에 들어가는 시점에 Update Cycle이 올 수 도 있고, 안올 수 도 있음. 아다리 잘 맞으면 업데이트 되서 부드럽게 보이는거고, 안맞으면 저래 뚝뚝 끊기는 것임.
와 어렵당 어려웡
다음은 setNeedsDisplay를 알아보자
이 분 글이 사기임 개쩜
https://tech.gc.com/demystifying-ios-layout/
'iOS > Swift' 카테고리의 다른 글
[Swift] VisionKit OCR (2) | 2024.10.07 |
---|---|
[Swift] UITableView, UICollectionView Last IndexPath (1) | 2023.12.05 |
[Xcode] 단축키 좀 써주세요... (0) | 2022.08.06 |
[Swift] defer (0) | 2022.06.10 |
[Swift] mutating (2) | 2022.05.08 |
댓글