iOS Layout Update

sanghoon Ahn·2023년 5월 4일
0

iOS

목록 보기
19/20
post-thumbnail

안녕하세요, szzang입니다!!

굉장히 오랜만의 포스트입니다 😅

그동안 놀고있었던건 아니고… 나름 여러가지 공부를 하느라 헿..

그래도 오늘은 오랜만에 묵혀두었던 공부하고 싶었던 주제에 대한 공부를 해보고 나름대로 정리한 내용을 공유해보려 합니다!

바로 iOS의 Layout은 어떻게 업데이트 되는가..!!

출발해 보시죠! 😎

Run Loop

먼저 iOS Layout Update를 이해하기 위해서는 Run Loop에 대해 알아야 합니다.

  • 쓰레드와 관련된 기본적인 인프라로써 작업이 예약된 이벤트의 수신을 조정하는데 사용하는 이벤트 처리 Loop로써 Run Loop의 목적은 수신된 이벤트가 존재한다면 쓰레드가 해당 이벤트를 처리하고, 수신된 이벤트가 없다면 쓰레드를 절전모드로 전환하기 위함입니다.
  • Run Loop는 완전히 자동으로 관리되지 않기 때문에 적절한 시간에 RunLoop를 시작하고 이후에 들어오는 이벤트에 응답하도록 쓰레드 코드를 설계해야 하며 Cocoa와 Core Foundation은 모두 Run Loop를 구성하고 관리하는데 도움이 되는 RunLoop객체를 제공합니다.
  • Application의 메인쓰레드를 포함한 각 쓰레드에는 연결된 RunLoop객체가 존재하기 때문에 명시적으로 생성할 필요는 없지만, 부수적인 쓰레드의 경우 명시적으로 RunLoop를 실행해야합니다.
  • 또한 App Framework는 application startup process의 일부로써 메인쓰레드에서 RunLoop를 자동으로 설정하고 실행합니다.
  • 즉 메인쓰레드에서는 RunLoop가 자동으로 설정되고 실행됩니다.

Run Loop의 상세구조

  • Run Loop는 쓰레드로 들어오는 이벤트에 대한 응답으로 이벤트 핸들러를 실행하는데 사용하는 Loop이며 코드로 Loop를 제공해야 합니다.
  • 즉, Loop코드를 제공하지 않는다면 Run Loop는 반복 실행하지 않습니다. → 왜 반복 실행하지 않을까?
    • 이벤트가 없다면 쓰레드를 절전모드로 만들기 위해서인것 같다. 이벤트가 없는데도 Run Loop가 무한히 반복한다면 쓰레드는 계속 작동하고 있을것이기 때문이다.
  • 코드로 제공된 Loop내에서 RunLoop는 이벤트를 수신하고, 핸들러를 수행합니다.
  • 단, 앞서서 메인쓰레드에서는 RunLoop가 자동으로 설정되고 실행되기 때문에 따로 Loop를 제공할 필요가 없습니다.

Main Run Loop

그럼 이제 Run Loop를 알아보았으니, iOS의 Layout이 어떻게 업데이트 되는지 본격적으로 알아보겠습니다.

  • Main Run Loop는 Run Loop의 한 종류로써 사용자의 이벤트를 관찰하고 응답하는 역할을 하고있으며 아래와 같은 flow로 동작합니다.

  1. 사용자의 이벤트 발생한다면
  2. event queue에 이벤트 저장
  3. Application에서 event queue에서 이벤트를 Application object로 전달
  4. Application Object에서는 이벤트를 해석하고 Application의 Core Object에 존재하는 Code를 실행할 수 있는 handler를 호출하여 Code가 호출 될 수 있도록 한다
  5. handler가 리턴되면 control이 handler에서 Main Run Loop로 넘어가고 이때 Update Cycle이 재실행된다

Update Cycle

  • Application이 이벤트 핸들링 코드를 모두 실행하고 Main Run Loop로 컨트롤을 다시 반환하는 시점까지의 동작
  • 이 동작에서 시스템은 View들을 Layout, Display, Constraint를 설정합니다.
  • 이벤트 핸들링 코드에서 View에 대한 변화를 준다면, 다음 Update Cycle에서 해당 View에 대한 모든 변경사항을 수행합니다.
  • iOS는 기본적으로 60FPS으로 동작(프로모션 디스플레이에서는 별도)하기 때문에 한번의 Update Cycle은 60분의 1초 주기로 실행됩니다.
  • 이벤트를 처리하는 시점과 View를 업데이트 하는 시점의 차이가 있기 때문에 원하는 시점의 Main Run Loop에서 View가 업데이트 되어있지 않을 수 있습니다.
    • 이벤트를 처리하면서 View의 변경사항이 생긴다면 다음 Update Cycle에서 반영하기 때문
  • 이렇게 된다면 업데이트 되지 않은 View를 조작할 수 있는 상황이 생길 수 있습니다.

Layout

  • Layout은 View의 크기와 위치를 의미하며 모든 View는 Frame을 가지고있습니다.
    • 이때, Frame은 Parent를 기반으로한 좌표와 크기를 나타내며, Bounds는 자기 자신을 기준으로 좌표계와 크기를 나타냅니다.
  • View는 자신의 Layout이 변경되었거나 Layout이 다시 계산되는 시점에 특정 작업을 실행할 수 있는 아래와 같은 method를 제공합니다.

layoutSubViews()

  • View와 subView들의 위치와 크기를 재조정합니다.
  • 이때 subView의 더욱 정확한 Layout을 위해서는 해당 메서드를 override하여 subView의 frame을 직접 설정 할 수 있습니다.
    • 기본적으로 제공되는 layoutSubViews의 동작으로 인해 layout과 constraint가 의도대로 동작하지 않는 경우에만 override해야 합니다.
  • 직접 해당 method를 호출하는것 보다 System에서 해당 method를 호출하도록 하는것이 System에 대한 부하가 적습니다.
    • 직접 호출하게되면 부하가 큰 이유는 subView의 크기와 위치를 모두 재귀적으로 계산하고 지정하기 때문입니다.
    • 그렇다면 왜 접근제한자를 통해 막아두지 않았을까?
      • override 해야하므로 → 그렇다면 직접 override 하는대신 closure를 전달하는 방법도 있지 않나?
  • layout update를 강제하기 위해서는 setNeedsLayout() method를 호출하여 다음 update cycle에서 View가 업데이트 되도록 할 수 있으며
  • 다음 update cycle 전에 View의 layout을 즉시 업데이트하려면 layoutIfNeeded() method를 호출해야합니다.
  • 위 두가지 method는 Main Run Loop가 진행되는 동안 layoutSubViews()가 호출되는 시점이 다릅니다.
  • layoutSubViews()가 완료될때 View를 소유한 ViewController의 viewDidLayoutSubViews가 호출됩니다.
    • layoutSubViews()는 View의 Layout이 변경되었다는 유일한 call back이기 때문에 뷰의 Layout에 대한 코드들은 viewDidLoad(), viewDidAppear()가 아닌 viewDidLayoutSubViews()에 위치시켜야 합니다.
      • 그렇다면 모든 Layout 코드들을 viewDidLayoutSubViews()에 위치시켜야 하나?
        • 그것은 아니라고 생각한다. subView들이 layout될 때 마다 viewDidLayoutSubViews()가 호출된다.
        • 따라서 모든 Layout을 viewDidLayoutSubViews()에 위치시키게되면 Layout이 변경될 필요가 없는 View까지 Layout을 갱신하게 될 것 같다.
        • 따라서 지속적으로 갱신이 필요한 View의 Layouy 코드를 해당 method안에 위치시키면 될 것 같다.

Automatic Layout Refresh Trigger

아래 동작들은 View가 자동적으로 Layout이 변화되었다는 flag를 변화시켜 다음 update cycle에서 layoutSubViews가 호출되게 됩니다.

  • View Resizing
  • SubView 추가
  • UIScrollView Scroll 시 ScrollView와 parent가 layoutSubViews를 호출
  • Device Orientation Change
  • View의 Constraint 변경

자동으로 layoutSubViews()가 호출되게 하는 동작이외에도 직접 호출되게하는 방법도 존재합니다.

setNeedsLayout()

update cycle을 준수하기 때문에 가장 적은 부하로 layoutSubViews()를 호출할 수 있는 method입니다.

setNeedsLayout()은 즉시 View를 update 하지 않지만 다음 cycle에 layoutSubViews()를 호출하여 update 될 수 있도록 합니다.

마찬가지로 View의 Layout 시점과 View가 실제로 다시 그려지는 시점이 일치하지 않을 수 있습니다.

layoutIfNeeded()

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이 올바르게 동작하도록 할 수 있습니다.

Display

Layout이 View의 크기와 위치를 의미했다면 Display는 View의 속성들 중 크기와 위치 그리고 Child View에 대한 정보를 제외한 모든 속성을 포함합니다.

예를들어 Color, Text, Image, Core Graphics가 있습니다.

Display는 Layout과 유사하게 System을 통해 업데이트를 하거나 직접 업데이트를 하는 method를 호출하는 방식이 있습니다.

draw()

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를 설정해서는 안됩니다.

setNeedsDisplay()

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를 호출해야 합니다.

Constraint

AutoLayout에는 아래와 같은 과정이 필요합니다.

  1. Constraint: Constraint를 업데이트하여 System이 View에 필요한 Constraint를 계산하고 설정합니다.
  2. Layout: Layout Engine이 VIew와 Child View에 대한 frame을 계산하여 layout 합니다.
  3. Display: View의 Contents를 다시그리고 필요하다면 draw()를 호출합니다.

updateConstraints()

Auto Layout을 사용하는 View의 Constraint를 동적으로 변경합니다.

layout의 layoutSubViews(), display의 draw()처럼 직접 호출해서는 안됩니다.

updateConstraints()에서는 일반적으로 동적으로 변하는 Constraint들을 구현하고, 정적인 Constraint들은 View의 생성자나 viewDidLoad()에 작성합니다.

Constraint를 활성/비활성화 하거나 priority 변경, constant 수정, View를 hierarchy에서 제거하는것은 System이 다음 update cycle에서 updateConstraints()를 호출하게합니다.

Layout, Display와 마찬가지로 명시적으로 호출하는 방법도 존재합니다.

setNeedsUpdateConstraints()

다음 update cycle에서 updateConstraints를 호출하게 하며, setNeedsLayout(), setNeesdDisplay()와 비슷하게 작동합니다.

View의 property가 Constraint에 영향을 미친다고 가정하고, 해당 property에 변경이 생긴다면 명시적으로 setNeedsUpdatedConstraints()를 호출하여 다음 update cycle에 updateConstraints()가 호출되게 할 수 있습니다.

이러한 방식은 여러 Constraint가 업데이트 될 때마다 영향을 받는 Constraint들이 다음 updateConstraints()에서 한번에 계산할 수 있도록 하여 최적화를 제공할 수 있습니다.

updateConstraintIfNeeded()

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를 호출하게 됩니다.

Connect

앞서 살펴본 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

https://sueaty.tistory.com/162

profile
hello, iOS

0개의 댓글