지난 포스팅에서 Main Run Loop과 Update Cycle에 대해 알아보았다. 

이 Update Cycle은 view들을 layout, constraints, display 하는 작업을 담당하였다.

Auto Layout에서 이들은 3단계를 거치게 되는데

  • Constraints: 시스템은 모든 constraints를 계산하고 설정한다.
  • Layout: View의 배치하고, subview를 배치한다.
  • Display: 필요한 경우 다시 그린다.

 

이번 포스팅에서는 layout, constraints, display가 어떻게 진행되고, 어떠한 메서드들이 있는지 알아보자

 

Layout 

view들의 position size들을 의미한다. 

view들의 Layout 변화는 layoutSubViews() 메서드에서 진행되는데, 

layoutSubViews()를 직접 호출하게 되면,

해당 view의 모든 subView들의 layoutSubViews() 메서드 재귀적으로 호출하게 된다.

즉, 해당 함수를 직접 호출하게 되면 매우 expensive하기 때문에 직접 호출하는 것을 금지한다. 

 

layoutSubviews()의 동작이 끝나면, 

해당 view를 가진 viewController의 viewDidLayoutSubViews() 가 호출이 된다.

따라서, 업데이트된 view의 메서드를 통한 로직을 구현하고 싶다면, viewDidLayoutSubViews()에서 진행하여야 한다.

 

그렇다면, layoutSubviews()는 어떠한 방법으로 호출시켜야 할까? 

 

첫번째는, layoutSubviews()의 호출을 자동으로 예약하는 것이다. 

이렇게 알리게 되면, 시스템은 변경이 필요한 view를 마킹

다음 Update Cycle때 시스템에 의해 layoutSubviews()가 호출되며, Layout이 변경된다.

 

layoutSubviews()의 호출을 자동으로 예약하는 방법은 다음과 같다.

  • View resizing
  • Subview 추가 
  • UIScrollView에서 스크롤 
  • device 회전
  • view의 constraints를 변경

 

두번째는, layoutSubviews()의 호출을 수동으로 예약하는 것이다.

setNeedsLayout(), layoutIfNeeded()를 통해 예약이 가능한데, 이 두 메서드에 대해 알아보자

 

 

setNeedsLayout()가 호출이 되게 되면,

해당 view는 "재계산이 필요한 view"로 마킹이 되고, 리턴된다.

다음 update cycle때, layoutSubviews가 호출이 된다.

이러한 이유 때문에, 애플 공식 문서에서는 가장 경제적인 방법이라고 소개한다.

얼핏 보면, 자동 예약하는 방법과 유사하지만, 

setNeedsLayout()의 경우에는 layoutSubviews()의 호출을 보증한다.

 

 

layoutIfNeeded()가 호출이 되게 되면, 

재계산이 필요한 view라고 마킹된 경우("재계산이 필요한 view" 라고 마킹된 경우) 라면, 

update cycle을 기다리는 것이 아닌, 즉시 layoutSubviews()를 호출하게 된다. 

반면, update가 필요한 경우가 아니라면, layoutSubviews()를 호출하지 않는다.

(자동 예약하는 방법 혹은 setNeedsLayout() 호출로 "재계산이 필요한 view"라고 마킹하여 update가 필요한 경우라고 알릴 수 있다.)

 

 

다음 코드는 button을 눌렀을때, view 가 업데이트되며, 

UIView.animate 전후로 시간차이를 출력하는 코드이다.

class ViewController: UIViewController {
    ...
    
    func currentTimeString() -> String {
       let f = DateFormatter()
       f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
       return f.string(from: Date())
    }
    
    @IBAction func buttonPressed(_ sender: Any) {
        if(layoutViewHeight.constant == 50) {
            layoutViewHeight.constant = 300
        }
        else {
            layoutViewHeight.constant = 50;
        }
        
        print(currentTimeString()) // before change layout

        UIView.animate(withDuration: 3.0) {
            self.view.layoutIfNeeded()
            //self.view.setNeedsLayout()
            } completion: { _ in
                print(self.currentTimeString()) // after animate
            }
    }
}

 

layoutIfNeeded()

layoutIfNeeded의 경우 

실제로 3초간의 차이가 나는 것을 볼수 있다. 

해당 동작을 살펴보면, 

 

layoutView의 constant를 업데이트하면, 

시스템은 "해당 view의 height를 다음 update cycle에 업데이트"를 예약하고,

animate의 클로져로 진입한다. 

 

animate의 closure에서 layoutIfNeeded()는 바로 return 되지 않고,

animate의 duration동안 지속되게 된다.

 

또한 layoutIfNeeded()는 layoutSubviews()를 바로 호출하기 때문에,

View는 animate의 duration동안, 연속적으로 업데이트되는 것처럼 우리 눈에 보이게 된다.

시스템은 "해당 view의 height를 다음 update cycle에 업데이트"를 예약하였지만, 

layoutIfNeeded()는 update Cycle을 기다리는 것이 아닌 즉각적으로 layoutSubViews()를 호출한다.

 

그렇기 때문에, 이를 동기 액티비티 라고 한다. 

 

setNeedsLayout()

반면, setNeedsLayout()의 경우에는 

 

animate가 적용되지 않으며, animate 전과 후의 시간 차이가 거의 나지 않는다. 

이러한 이유에 대해 알아보자!

closure까지 진입하는 구간은 layoutIfNeeded()와 동일하다.

 

아까도 말했듯이 setNeedsLayout()은 해당 view를 다음 update cycle에

update하도록 시스템에 알리게 되면서 바로 리턴한다.

 

여기서 중요한 것은 바로 리턴한다는 것인데, 

animate의 closure에서 setNeedsLayout()은 바로 리턴되기 때문에, 

animate의 동작은 duration만큼 진행되지 않고 바로 종료된다. 

또한, 시스템은 "해당 view의 height를 다음 update cycle에 업데이트"를 예약하였고, 

setNeedsLayout() 역시 바로 리턴되면서 "다음 update Cycle에 업데이트해줘"라고 예약하였기 때문에!

view는 다음 update cycle에 바로 업데이트가 된다.

 

이러한 특성 때문에 setNeedsLayout()은 비동기 액티비티이다.

 

 

 

Display

display 단계에서는 draw(_:) 메서드를 통해 실제로 그려지게 된다. 

 

draw(_:) 메서드는 layout 에서 layoutSubviews()와 유사하지만,

이는 subview를 재귀적으로 호출하지는 않는다.

또한, 직접 호출하면 안된다. 

 

layout 혹은 constraints의 변화로 다시 그려야 하는 경우 시스템은 draw(_:) 메서드를 자동 예약하고, 

특정 메서드를 통해 수동 예약할 수도 있다. 

 

setNeedsDisplay() 

이 method 역시 setNeedsLayout()과 유사하다. 

다음 update cycle에 view를 다시 그리도록 시스템에 알리며, 바로 리턴한다.

 

display 단계에서 layoutIfNeeded() 와 같은 즉각적으로 업데이트가 되도록 하는 메서드는 없다

거의 대부분의 경우에는 다음 update cycle까지 기다리는 것으로도 충분하다. 

 

 

 

Constraints

Constraints의 경우에도 이전의 draw(_:), layoutSubviews()와 같이, 

updateConstraints()에서 실제 constraints의 변화가 반영이 된다.

이들은 optimize가 필요한 경우에만 재정의를 진행하고,

이역시 직접 호출하면 안된다!

 

 

이들은 view 계층 구조에서 view를 제거하거나 constraints값을 변경하는 경우, 

시스템에 의해 updateConstraints()의 호출을 자동으로 예약하게 된다.

 

updateConstraints()의 호출을 수동 예약하는 방법

setNeedsUpdateConstraints()

해당 method는 setNeedsLayout()과 비슷하다. 

이 역시, 다음 Update Cycle에서 해당 "view의 constraints를 재계산해야 함"을 알리고, 바로 리턴된다. 

또한 이는 updateConstraints()의 호출을 보증한다.

 

invalidateIntrinsicContentSize()

 

Auto Layout를 사용하는 특정 View(UIButton, UILabel ...)는 constraints에 따른  instrinsicConstentSize라는 프로퍼티를 가지고 있는데, View의 content에 의해 결정되는 natural size(고유 사이즈)이다.

invalidateIntrinsicContentSize() 메서드를 instrinsicConstentSize를 다시 계산하도록 시스템에 알린다. 

이렇게 되면, 시스템은 다음 update cycle에서 updateConstraints()를 호출할 것을 보증한다.

 

 

updateConstraintsIfNeeded()

해당 method는 setNeedsLayout()와 유사하다. 

"재계산이 필요한 view" 라고 마킹된 경우라면 (다음 update cycle에서 update가 예약된 경우)

updateConstraints()를 즉시 호출한다.

(이역시, 자동 예약하게 하는 방법 혹은 수동 예약 (invalidateIntrinsicContentSize, setNeedsUpdateConstraints의 호출) 을 통해 "재계산이 필요한 view" 라고 마킹을 할 수 있다.)

 

 

Summaray 

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

 

  Layout Constraints Display
Update(실제 업데이트 진행)
직접 호출 X
layoutSubviews updateConstraints draw
다음 Update Cycle 기다림 
(Update method 호출을 보증)
setNeedsLayout setNeedsUpdateConstraints
invalidateIntrinsicContentSize

setNeedsDisplay
즉시 업데이트 진행
(필요하지 않는 경우 X)
layoutIfNeeded updateConstraintsIfNeeded X

 

 

 

Reference

https://github.com/lmacfadyen/UIViewLifecycleLayoutDisplay

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

 

Layout

 

Constraints 

복사했습니다!