iOS/개념

[iOS] iOS Layout 정리

누알라리 2021. 12. 9. 00:22

위 포스팅들을 참고해 정리한 글 입니다.

https://tech.gc.com/demystifying-ios-layout/

 

Demystifying iOS Layout

Some of the most difficult issues to avoid or debug when you first start building iOS applications are those dealing with view layout and content. Often, these issues happen because of misconceptions about when view updates actually occur. Understanding ho

tech.gc.com

https://medium.com/mj-studio/%EB%B2%88%EC%97%AD-ios-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83%EC%9D%98-%EB%AF%B8%EC%8A%A4%ED%84%B0%EB%A6%AC%EB%A5%BC-%ED%8C%8C%ED%97%A4%EC%B9%98%EB%8B%A4-2cfa99e942f9

 

[번역] iOS 레이아웃의 미스터리를 파헤치다

iOS의 UIView가 그려지는 과정과 메소드들을 살펴봅니다.

medium.com

 

 


노션으로 보면 더 편합니다.

https://acoustic-string-66e.notion.site/iOS-Layout-644934b59848464fa3474ebcba72f01c

 

개요

View가 언제 Update 되는지 정확히 이해하기 위해서는 이 두가지를 파악하는 것이 매우 중요하다.

  • iOS App의 Main run Loop
  • UIView가 제공하는 몇몇의 메서드들과의 관계성

 

iOS App의 Main run loop

1️⃣ 역할

iOS의 Main run loop는 유저로부터의 모든 input 이벤트를 받고, 적절한 응답을 해주는 것을 담당한다.

2️⃣ 동작방식

  1. 유저의 input이 들어오면...
  2. OS를 통해 Event Queue에 추가된다.
  3. Application Object가 Event Queue에서 Event를 하나 꺼내 그를 해석한다.
  4. 그에 상응되는 Application의 Core Object들 안에 있는 핸들러(handler)를 호출한다.
  5. 핸들러들이 개발자가 쓴 메서드들을 호출한다.
  6. 메서드들이 반환되면 다시 컨트롤은 main run loop으로 돌아가서 Update Cycle이 다시 시작된다.
  7. 여기서 Update Cycle은 View들을 배치하고 다시 그린다

 

Update Cycle

Update Cycle은 App이 유저로부터의 모든 이벤트 핸들링 코드를 수행하고, 다시 Main run loop로 컨트롤을 반환하는 지점이다.

바로 이 지점에서 OS는 App의 View들을

  • 배치하고(layout)
  • 보여주고(display)
  • 제약한다(constraints)

만약 어떠한 이벤트 핸들러를 처리하는 과정에서 어떤 UIView에 변화를 준다면, 그 UIView는 다시 그려져야(redraw) 한다고 표시된다.

그럼 다음 Update Cycle에서 OS는 이 UIView의 모든 변화를 수행한다.

 

유저가 App과 상호작용을 하는 것과 레이아웃이 변하는 시간의 갭은 유저가 인지하지 못할 정도여야 한다.

iOS App은 초당 60 프레임을 보여주기 때문에, 한 번의 Update Cycle은 1/60초 밖에 안된다.

 

매우 빠르기 때문에 유저는 인식하지 못하지만, 이벤트가 처리되는 시점(핸들러 처리되는 시점)과 실제로 View가 다시 그려지는 시점(Update Cycle)에는 차이가 있기 때문에, View는 개발자가 View를 업데이트 하기를 원하는 run loop의 특정 시점에 업데이트되지 않을 수 있다.

이 같은 문제는 예전 레이아웃 정보를 갖고 View를 조작할 수 있기 때문에 확실한 이해가 필요하다.


Layout

화면에서 UIView의 크기와 위치를 의미한다.

모든 View는 frame을 갖고 있고, 이는 부모 뷰의 좌표계에서 어디에 위치하고 얼마나 크기를 차지하는지 나타낸다.

UIView는 시스템에게

  • UIView의 Layout이 변했다고 알려줄 수 있는 메서드,
  • View의 layout이 다시 계산되는 시점에 특정한 작업을 실행할 수 있게 오버라이딩할 수 있는 콜백 메서드

같은 것들을 제공한다.

💡 layoutSubviews, viewDidLayoutSubviews, setNeedsLayout, layoutIfNeeded

 

UIView의 Layout에 변화가 생겼어요!!! 콜백!!!

layoutSubviews()

정의

UIView의 메서드

View와 그 자식 View들의 위치와 크기를 재조정한다.

 

작동방식

재귀적으로 모든 자식뷰의 layoutSubviews() 까지 호출해야해서 실행 시 부하가 큰 메서드이다.

시스템은 layoutSubviews를 뷰의 frame을 다시 계산해야 할 때 호출하기 때문에,

개발자는 layoutSubviews를 오버라이딩해서 frame이나 특정한 위치와 크기를 조절할 수 있다.

 

사용규칙

레이아웃을 업데이트할 때 layoutSubviews를 직접 호출하는 것은 금지 되어있다!!!

대신, 시스템이 layoutSubviews를 호출하도록 유도할 수 있는 여러 개의 방식이 존재한다.

이런 방식들은 모두 layoutSubviews가 실행되는 시점이 다 다르며, 직접 호출하는 것보다는 부하가 덜하다.

 

viewDidLayoutSubviews()

작동방식

layoutSubviews가 완료될 때, viewDidLayoutSubviews가 View를 소유한 ViewController에서 호출된다.

layoutSubviews는 view의 layout이 변화했다는 유일한 콜백이기 때문에

개발자는 레이아웃의 크기나 위치와 연관된 로직을 viewDidLoad나 viewDidAppear가 아닌, viewDidLayoutSubviews에 정의해야 한다.

이것이 레이아웃이나 위치 변수의 계산 실수를 막는 유일한 방법이다.

 

시스템아!! layoutSubviews()를 호출해라!!! 유도!!

Automatic refresh triggers

다음 이벤트들은 자동으로 View의 Layout에 변화가 생겼다고 표시해주어 개발자가 직접 호출할 필요 없이 layoutSubviews가 다음 루프 때 호출 되게 한다!!

  • View를 Resizing
  • Subview를 추가
  • UIScrollView를 스크롤 할 때, UIScrollView와 그것의 부모뷰에 layoutSubviews가 호출
  • Device 회전 (orientation change)
  • View의 Constrait를 변경

위 trigger들은 자동으로 시스템이 UIView의 위치가 변했고, 다시 계산되도록 layoutSubviews() 가 호출되게 해준다.

그러나 layoutSubviews를 직접 호출해줄 수 있는 방법들도 존재한다.

 

가장 적은 부하로 layoutSubviews를 호출해줘!!!

setNeedsLayout()

정의

layoutSubviews를 가장 적은 부하로 호출하는 메서드.

setNeedsLayout()은 시스템에게 이 View의 layout이 다시 계산되어야 한다고 알려준다.

 

작동방식

setNeedsLayout은 호출 즉시 반환되고, 반환 전에 실제로 View를 업데이트해주는 것은 아니다.

대신, 시스템이 다음 Update Cycle에서 layoutSubviews를 View와 자식 View들에게 호출하게 한다.

이 때 setNeedsLayout이 반환되고 View가 다시 그려질 때 까지 잠깐의 interval 타임이 있긴하지만, 유저들에게는 렉이라고 느껴질만큼의 딜레이가 느껴지진 않는다!!

 

지금 당장 layoutSubviews를 호출해줘!!

layoutIfNeeded()

정의

UIView가 layoutSubviews 함수를 호출하도록 하는 또 다른 명시적인 메서드

 

동작방식

layoutSubviews가 다음 Update Cycle에서 호출되도록 하는 것이 아닌,

View의 Layout이 재조정되어야 하는 상황 다음에 불린다면 즉시 LayoutSubviews를 호출한다.

 

하지만 layoutIfNeeded를 호출했는데 View가 재조정되어야 하는 이유가 없다면, layoutSubviews는 호출되지 않는다.

만약 우리가 동일한 run loop에서 레이아웃의 업데이트 없이 layoutIfNeeded를 호출했다면, 두번째 호출은 layoutSubviews를 발생시키지 않는다.

 

View의 Layout이 재조정 되어야 하는 상황

  • setNeedsLayout이 호출된 직후
  • Automatic refresh Trigger 들 중 하나가 불린 직후

 

setNeedsLayout과의 차이점

  • layoutIfNeeded를 호출한다면, 레이아웃을 하는것과 자식 View들을 다시 그리는 것은 이 메서드가 반환되기 전에 즉시 실행됩니다. (애니메이션 상황은 제외하고)
  • setNeedsLayout과 다르게, 이 메서드는 Update Cycle까지 뷰의 변화를 기다릴 수 없는 상황에서 유용

하지만, 이런 즉시 업데이트가 필요한 상황이 아니라면, 그냥 setNeedsLayout을 호출해서 run loop 한 번당 View 업데이트가 한 번만 이루어지게 하는것이 이상적이다.

 

 

애니메이션

💡 layoutIfNeeded는 Constraints를 애니메이션하는 상황에서 특히 유용하다.
  • 우리는 애니메이션이 시작하기 전에 LayoutIfNeeded를 호출해서 모든 레이아웃 업데이트가 애니메이션 전에 수행되도록 전파할 수 있다.
  • 새로운 Constraints를 설정하고,
  • 애니메이션 클로저 안에서는 또 LayoutIfNeeded를 호출해서 애니메이션이 올바른 상태로 진행되도록 할 수 있다.

Display

뷰의 속성들 중 크기, 위치, 자식 View에 대한 정보를 나타내지 않는 속성들을 포함한다.

(Ex. 색, 텍스트, 이미지, Core Graphics 그리기 등)

 

Display는 Layout과정과 유사한 방식으로 동작한다.

  • 시스템이 자동으로 업데이트 되게하는 방식
  • 명시적으로 업데이트를 해주게 하는 방식(메서드)

Layout과는 다르게 즉시 draw(:)를 호출하는 layoutIfNeeded() 같은 함수는 없다.

draw(_:)

Layout 업데이트 과정에서의 layoutSubviews와 같은 역할을 한다.

큰 차이점은 draw 메서드는 자식 View들의 draw까지 호출하지는 않는다.

마찬가지로 직접 호출하는 것은 좋지 않다.

 

setNeedsDisplay()

동작방식

  1. View의 Content가 업데이트 되게하는 내부 dirty 플래그를 활성화 시킨다.
  2. 실제로 View가 다시 그리기 전에 메서드는 반환된다.
  3. 다음 Update Cycle에 시스템은 플래그가 활성화되어있는 View들을 draw를 호출해서 다시 그려준다.

사용법

만약 View의 일부분만 다시 그려지길 원한다면, setNeedsDisplay 메서드의 인자로 rect를 전달할 수 있다.

대부분 View의 UI 컴포는터를 업데이트하는 것은 View 내부의 dirty 플래그를 활성화 시켜서 우리가 명시적으로 setNeedsDisplay를 호출하지 않아도 다음 Update Cycle에서 뷰가 다시 그려지도록 유도한다.

 

하지만, 만약 UI 컴포넌트와 직접적 연관이 없지만 매 Update Cycle 마다 다시 뷰를 그려주어야 하는 속성이 있다면, 우린 didSet 감시자를 설정하고 setNeedsDisplay를 명시적으로 호출할 수 있다.

예시

class MyView: UIView {
    var numberOfPoints = 0 {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        switch numberOfPoints {
        case 0:
            return
        case 1:
            drawPoint(rect)
        case 2:
            drawLine(rect)
        case 3:
            drawTriangle(rect)
        case 4:
            drawRectangle(rect)
        case 5:
            drawPentagon(rect)
        default:
            drawEllipse(rect)
        }
    }
}

Constraints

Auto Layout의 세계에서는 Layout하고 Draw하는 것에 대해 3단계의 과정이 있다.

  1. Constraints를 업데이트 한다.
    • 시스템이 View에 필요한 Constraints들을 계산하고 설정한다.
  2. Layout 단계
    • 레이아웃 엔진이 View들의 Frame과 자식 View들의 Frame을 계산하고 배치한다.
  3. Display 단계
    • View의 컨텐츠를 다시 그리고 필요하다면 draw 메서드를 호출한다.

updateConstraints

정의

Auto Layout을 이용하는 View의 Constraints를 동적으로 변경할 때 사용한다.

오직 오버라이딩 되어야 하며 명시적으로 호출되어서는 안된다.

보통 동적으로 변하는 Constraints들을 구현한다.

 

작동방식

일반적으로,

  • Constraints 활성화/비활성화
  • Constraints 우선순위 / constant 변경
  • View를 View 계층에서 삭제하는 것

이 update Constraints를 다음 Update Cycle에서 호출하게 한다.

 

setNeedsUpdateConstraints

다음 Update Cycle에서 Constraint가 업데이트 되는것을 보장한다.

 

updateConstraintsIfNeeded

오직 Auto Layout을 사용하는 뷰에만 유효하다.

이 메서드는 Constraint Update Flag를 검사한다.

만약 Constraint가 업데이트 되어야 하면, updateConstraints를 즉시 호출한다.

 

invalidateIntrinsicContentSize

Auto Layout을 사용하는 몇몇 View들은 intrinsicContentSize 속성을 갖는다. 이는 View가 갖고 있는 Content의 크기이다.

intrinsicContentSize는 전형적으로 View가 갖고있는 요소들의 Constraints로 결정되지만, 이것 또한 커스텀한 동작을 오버라이딩하여 제공할 수 있다.

invalidateIntrinsicContentSize를 호출하는 것은 view가 갖고있는 intrinsicContentSize가 잘못되었으며, 다음 Update Cycle에서 다시 계산되어야 한다고 플래그를 활성화 시켜준다.


총 정리

View의 Layout, Display, Constraints는 run loop에서 다른 시점에 어떻게 업데이트되고, 명시적으로 업데이트 할 수 있는지에 대해 유사한 패턴을 갖는다.

 

각 컴포넌트들은

  • layoutSubviews
  • draw(_:)
  • updateConstraints

와 같은 실제로 업데이트를 전파하는 메서드들을 가지며,

이들을 명시적으로 호출하면 안되기 때문에 이를 호출하도록 유도할 수 있는 방법들이 있다.

 

이러한 메서드들은 run loop의 마지막에 View의 해당 flag가 활성화 되어있으면 시스템이 호출해주는 방식이다.

자동적으로 이 flag들을 활성화해주는 방식, 명시적으로 활성화 시켜주는 방식이 있다.

 

layout과 constraints에는 관련된 업데이트들에 대해서 만약 다음 update Cycle까지 기다릴 수 없다면, 즉시 업데이트가 되도록 요청하는 메서드들도 존재한다.