티스토리 뷰

iOS 개발을 하면서 매일 뷰와 사용자와 interaction이 가능한 다양한 UI Components를 만들지만
"View Rendnering" 에 대한 이해가 부족함을 느꼈습니다. View에 대한 이해가 부족하니 문제 해결도 잘 안됐던 것 같아요.
에러나 버그가 생기면 무작정 뷰 디버거를 열고 이리저리 돌려보고 view hierarchy를 봐도 해답을 못 찾기 일쑤였죠.
그래서 Layout, Display 그리고 Constraint를 관장하는 Update Cycle에 대해 정리해보겠습니다.

 

Main Run Loop와 Update Cycle

Main run loop와 Update Cycle에 대해 공부를 해보셨다면 아래 두 다이어그램을 한 번 쯤은 보셨을 것 같아요.
좌측에는 앱이 실행된 후 사용자가 주는 이벤트에 의한 반응을 표시하는 과정이 나타나있고
우측은 main run loop와 update cycle의 실행 주기가 나타나있습니다. 전체적인 과정을 같이 살펴볼까요?

 

  • 사용자의 input(이벤트)이 발생하면 해당 이벤트를 event queue에 추가
  • Application object가 event queue에서 하나씩 가져와서 작업을 수행 할 core object에게 분배
    • 엄밀히 말하면, 앱의 core object들 중 일을 수행할 수 있는 handler 호출
  • Handler가 개발자의 코드를 호출해서 실행
  • 개발자가 작성한 코드가 return 되면 control이 handler에서 main run loop로 넘어감
  • Update cycle 시작
    • 앞으로 다룰 내용이지만 update cycle에서 view의 layout, display, constraint에 관여합니다!
  1.  

Update Cycle

Update Cycle은 event handling code를 모두 실행하고 control을 다시 main run loop가 갖게되면 시작됩니다.
이 때 layout, display 그리고 constraint 들을 업데이트하게 되죠!
이벤트를 처리하면서 view 변화가 생기면 "다음 update cycle 때 나 처리해죠!"라고 update flag를 켜서,
시스템이 update cycle 때 변경사항들을 적용할 수 있습니다.

 

그러면 이런 의문이 들겠죠?

"당장 업데이트하는 것이 아니라 다음 update cycle 때까지 기다리면 사용성을 해치지 않을까?"

 

물론 한 cycle을 기다려야하기 때문에 delay가 발생하기는 합니다.
그런데 iOS application의 refresh cycle은 일반적으로 1/60초가 걸려요. 사실상 사용자가 체감할 수 없는 시간입니다.
그래도,

 

  • 실제 이벤트가 처리되는 시점과 개발자가 뷰 업데이트를 원하는 시점이 다름
  • 당장 업데이트가 필요할 수도 있음
  • 코드 내의 state 값을 사용해서 뷰를 그릴 경우, 시스템에게 "이거 뷰 그릴 때 필요하다?!" 알려줘야 함

그래서 UIView에서 제공하는 메소드들을 잘 사용하면 원하는 바를 효과적으로 이룰 수 있습니다.

Update Cycle : Layout → Display → Constraint

바쁘신 분들을 위해 오늘의 결론부터 가져왔습니다. 시간있으시면 아래 글도 쭈욱 읽어주세요 ;)

Update cycle에서는 layout, display, constraint 관련 된 변경사항들을 적용해주는데요,
UIView의 layout, display, constraint가 무엇을 뜻하는지 알고 계신가요?
메소드를 알아보기 전에 각각의 항목이 무엇을 뜻하는지 먼저 살펴보고 갈게요!

  • Layout : View의 크기 + Screen에서의 View의 위치 
    모든 view는 자신의 크기와, superview의 좌표계에서 어디 있는지 나타내는 frame을 갖고 있기 때문에
    UIView는 view의 크기나 위치의 변동사항을 system에게 알려주고 layout을 업데이트합니다.
  • Display : Color, Text, Image, CG Drawing 과 같이 size, position과 관련 없는 property
  • Constraint : View의 Layout을 정의하고 있는 규칙

업데이트가 필요하다고 알려주는 방법 두가지가 있습니다.

위에 언급 된 update flag를 통해 시스템이 변화를 알아서 감지할 수 있게끔 하는 방법이 있고,

refresh를 위해 개발자가 직접 호출할 수 있는 메소드들도 있습니다. 이제 메소드들에 대해 살펴볼까요?

Layout

layoutSubviews()

View 및 그 Subview들의 크기를 다시 계산하거나 위치의 재지정을 다루는 메소드입니다.

Frame의 변화에 따라 크기나 위치를 다시 계산할 필요를 느끼면 시스템이 layoutSubviews()를 호출합니다.

그래서 frame을 지정하거나 크기, 위치 정보를 구체적으로 정해주고 싶으면 override해서 전달하면 됩니다.

현재 계산 중인 view와 subview들의 크기와 위치 정보를 모두 재귀적으로 계산하고 지정하기 때문에
시스템에 큰 부하를 주게 됩니다.

 

layoutSubviews()가 끝나면 viewDidLayoutSubviews()가 불리는데요.

layoutSubviews가 view의 layout이 업데이트 되고 반드시 불리는 유일한 메소드라서 layout, sizing 관련 된 로직은
viewDidLayoutSubviews()에
 두는 것이 맞습니다. (viewDidLoad, viewDidAppear 아니에요!)
개인적으로 infinite carousel view를 구현할 때 UICollectionView가 load 되자마자 scrollToItem 메소드를 통해

원하는 n번째 cell로 옮겨가게끔 만든 경험이 있는데 [UICollectionView로 Infinite carousel 만들기]에서 확인해보실 수 있습니다~

 

Layout을 refresh하고 싶어도 이 메소드는 절대 직접 호출하면 안된다고 공식문서에 명시되어 있습니다.

대신 layoutSubviews()가 불리게끔 하는 방법들이 있으니 시스템에 부하가 덜 가는 방법을 사용하면 됩니다.

1) 자동적으로 불리는 경우가 있고 2) 개발자가 필요에 의해 호출하는 메소드가 있습니다.

자동적으로 Update Flag 켜기

자동적으로 update flag가 켜지면 다음 update cycle 때 시스템이 layoutSubviews()를 호출해서 변경사항을 적용합니다.

  • 기기 회전
  • subview 추가
  • view 크기 변경
  • view의 constraint 변경
  • (사용자가) UIScrollView 스크롤(layoutSubviews는 UIScrollView와 그 superview에서 불림)

개발자가 직접 부르기

setNeedsLayout()

setNeedsLayout()이 호출되면 시스템에게 view의 layout을 다시 계산해야한다고 알려줍니다.

그러나 메소드가 호출되고 바로 return이 되는데요, layout의 업데이트를 다음 update cycle에서 적용하기 때문이에요.

다음 update cycle에서 layoutSubviews()가 불릴 때 해당 view와 그 subview들에 모두 적용 되고,
layoutSubviews()를 호출할 수 있는 가장 저렴한(부하가 적은) 방법입니다.

layoutIfNeeded()

layoutIfNeeded()가 호출되면 setNeedsLayout과 다르게 시스템이 바로 layoutSubviews()를 호출합니다.

호출 한 view에 dirty flag가 켜져서 시스템에게 바로 변경사항 적용하라고 전달하게 됩니다.

그러나 호출한다고 무조건 layoutSubviews를 부르지는 않는데요, 불리는 case에 대해 정리해보았습니다!

  • Update Flag가 켜져있고 → layoutIfNeeded 호출 ⇒ 불림
  • setNeedsLayout 호출을 앞에 했고 → layoutIfNeeded 호출 ⇒ 불림
  • update flag / setNeedsLayout → layoutIfNeeded 호출 → 변동사항 없음 → layoutIfNeeded 호출
    ⇒ 첫 layoutIfNeeded에서는 layoutSubviews 불리지만 두번째는 불리지 않음

즉, view가 refresh 되어야 함을 가리키는 것들이 있으면 layoutSubviews가 불리지
시스템이 필요성을 느끼지 못하면 layoutIfNeeded가 아무리 있어도 layoutSubviews를 호출하지 않아요.

 

layoutIfNeeded도 시스템에 부하를 주는 메소드라 setNeedsLayout을 부르는 것을 추천합니다.

layoutIfNeeded가 필요한 경우는 언제일까요?

  • 당장의 layout에 의존 된 업데이트를 해야해서 다음 update cycle을 기다릴 수 없을 때
  • constraints에 애니메이션을 적용할 때⇒ 애니메이션 코드 블럭이 불리기 전에 layoutIfNeeded를 호출해서 업데이트 예정인 모든 layout을 다 적용하고constraint를 수정한 후 애니메이션 블럭 안에 layoutIfNeeded를 다시 불러서 애니메이션을 적용

Display

draw(_:)

layoutSubviews 처럼 동작하지만 재귀적으로 하위 view들에게는 적용되지 않는다는 차이점이 있어요.

마찬가지로 직접적으로 호출하면 안되는 메소드니 update flag를 켜는 방법과 개발자가 직접 부르는 방법을 살펴보죠!

자동적으로 Update Flag 켜기

  • view의 bound 변경

개발자가 직접 부르기

setNeedsDisplay()

setNeedsDisplay를 호출하면 update flag를 켜지만 업데이트는 다음 update cycle에서 적용됩니다.
다음 update cycle에서 flag가 켜진 모든 view들에 대해 draw(:_)를 호출하게 되죠.
대부분의 경우 UI components에 변화가 생기면 자동으로 다음 cycle에 redraw를 합니다. 그렇지 않은경우가 있을까요?
네, UI Components에 직접적인 연결이 되어 있지 않는 custom drawing은 setNeedsDisplay를 지정해주어야 합니다.

 

다음과 같은 상황을 상상해볼까요? 사용자의 숫자 입력값에 따라 보여지는 도형이 달라지는 로직입니다.

class UserShape: UIView {
   var numberOfSides = 0 {
      didSet { setNeedsDisplay() }
   }

   override func draw(_ rect: CGRect) {
      switch numberOfSides {
         case 0: return
         case 1: drawPoint(rect)
         case 2: drawLine(rect)
         case 3: drawTriangle(rect)
      }
   }
}

이런 경우라면 custom drawing 로직은 draw(_ :)를 override 하여 구현해놓고,
setNeedsDisplay를 property observer 내에 두는 방법을 택할 수 있습니다.

Constraint

Auto Layout을 적용할 때 view를 화면 위에 놓고(크기와 위치를 판단 - layout) 그리기(draw)까지 3 단계를 거칩니다.

  1. Constraints 업데이트 : 시스템이 view가 필요로하는 constraint를 계산하고 지정
  2. Layout : Layout 엔진이 현재 view와 subview들의 frame을 계산하고 지정
  3. Display : Update cycle의 마지막 단계로, 필요에 따라 draw 메소드를 호출해서 view의 컨텐츠들을 그림

updateConstraints()

Auto Layout를 사용하는 view가 동적으로 변화하는 constraints를 적용할 수 있게끔 해주는 메소드로
역시 직접 호출은 금지됩니다.
 사용할 땐 override해서 동적으로 변화하는 constraint들을 구현하는 것이 바람직하고
정적인 constraints들은 Interface Builder로 잡거나, view의 init 또는 viewDidLoad에 구현해는 것을 추천해요.

자동적으로 Update Flag 켜기

  • view 계층에서 view 삭제
  • constraints 활성화 / 비활성화
  • constraints들의 우선순위(priority)나 상수값 변경

개발자가 직접 부르기

setNeedsUpdateConstraints()

Constraint가 다음 update cycle에서 업데이트 할 수 있음을 보장하는데요,
이 쯤되니 생김새와 하는 역할이 setNeedsLayout과 setNeedsDisplay와 같죠?

updateConstraintsIfNeeded()

updateLayoutIfNeeded와 비슷하게 생겼으니 다음 cycle을 기다리지 않고 바로 updateConstraints를 호출할 것 같죠?
네, 맞습니다! updateConstraintsIfNeeded가 불리면 update flag를 확인한 후 update가 필요하다고 판단되면
updateConstraints를 바로 호출하는 메소드입니다. 역시 시스템에 부하가 많이 가는 메소드입니다.

invalidateInstrinsicContentSize()

우선 intrinsic content size가 무엇인지 알아보겠습니다.
Intrinsic content size는 view의 contents가 이상적으로 나타나려면 필요한 공간입니다.
가령 UILabel의 contents는 내부에 있는 text입니다.
그래서 intrinsic content size는 폰트와 무관하게 그 text의 크기가 되겠습니다.
Auto Layout을 적용하려면 view의 X, Y 좌표와 높이, 길이를 알아야 하는데
intrinsic content size를 알면 "수평으로 중심을 맞추고 top에서 20point 띄워"라고 지정할 수 있기 때문에 중요합니다.

 

그런데 invalidate한다고 하죠?
이 메소드를 통해 view의 contents가 변경되면 intrinsicContentSize 프로퍼티를 통해 크기 계산을 다시 할 수 있습니다.
물론 Apple에서 제공되는 view들은 자동적으로 update flag가 켜지기 때문에 다음 update cycle에 갱신이 되지만
custom view를 만들었을 경우에는 intrinsicContentSize 프로퍼티와 invalidateIntrinsicContentSize()를 구현해주어야 합니다.

 

'iOS' 카테고리의 다른 글

[iOS] lazy var : 메모리 세이버 🦸‍♀️  (4) 2021.07.28
[iOS] NotificationCenter는 중앙우체국  (0) 2021.07.27
[Rx] Reusable Cell의 흔적이 남는다  (0) 2021.05.17
[Rx] DisposeBag  (0) 2021.05.17
[Rx] UITableViewCell 밀어서 삭제  (0) 2021.05.13
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함