※본 글은 아래 링크의 문서를 번역한것입니다 (구글 번역으로 보면 명시된 객체 이름들이 뒤죽박죽되서 보기편하게 하기위해서 적습니다.)
https://tech.gc.com/demystifying-ios-layout/
iOS 레이아웃 이해하기
iOS 애플리케이션 구축을 처음 시작할 때 피하거나 디버깅하기 가장 어려운 문제 중 하나는 뷰 레이아웃 및 콘텐츠를 다루는 문제입니다. 이러한 문제는 뷰 업데이트가 실제로 발생하는 시기에 대한 차이로 인해 발생하는 경우가 많습니다.뷰가 업데이트되는 방법과 시기를 이해하려면 iOS 애플리케이션의 기본 실행 루프와 이것이 UIView에서 제공하는 일부 메서드와 어떻게 관련되는지에 대한 더 깊은 이해가 필요합니다 이 블로그 게시물에서는 이러한 상호 작용에 대해 설명하고 원하는 동작을 얻기 위해 UIView의 메서드를 사용하는 방법을 명확하게 설명할 것입니다.
iOS 앱의 메인 실행 루프
iOS 애플리케이션의 기본 실행 루프는 모든 사용자 입력 이벤트를 처리하고 애플리케이션에서 적절한 응답을 실행하는 것입니다.애플리케이션과의 모든 사용자 상호 작용은 이벤트 대기열에 추가됩니다.아래 다이어그램에 표시된 애플리케이션 개체는 대기열에서 이벤트를 가져와 애플리케이션의 다른 개체로 전달합니다. 이는 기본적으로 사용자의 입력 이벤트를 해석하고 애플리케이션의 핵심 개체에서 해당 입력에 해당하는 핸들러를 호출하여 런 루프를 실행합니다. 이러한 핸들러는 애플리케이션 개발자가 작성한 코드를 호출합니다. 이러한 메서드 호출이 반환되면 제어가 기본 실행 루프로 반환되고 update cycle가 시작됩니다. 업데이트 주기는 뷰 레이아웃 및 redrawing views를 담당합니다(다음 섹션에서 설명).
다음은 애플리케이션이 장치와 통신하고 사용자 입력을 처리하는 방법을 보여줍니다.
업데이트 주기 (Update cycle)
업데이트 주기는 앱이 모든 이벤트 처리 코드 실행을 마친 후 제어가 기본 실행 루프로 반환되는 지점입니다. 이 시점에서 시스템은 Layout, Display 및 Constraints를 업데이트하기 시작합니다. 이벤트 핸들러를 처리하는 동안 뷰의 변경을 요청하면 시스템은 뷰를 다시 그리기가 필요한 것으로 표시합니다. 다음 업데이트 주기에서 시스템은 이러한 뷰에 대한 모든 변경 사항을 실행합니다. 사용자 상호작용과 레이아웃 업데이트 사이의 지연은 사용자가 인지할 수 없어야 합니다. iOS 애플리케이션은 일반적으로 60fps로 애니메이션됩니다. 즉, 한 번의 새로 고침 주기는 1/60초에 불과합니다. 이 작업이 빨리 발생하기 때문에 사용자는 장치에서 애플리케이션과 상호 작용하고 콘텐츠 및 레이아웃 업데이트를 확인하는 사이에 UI에서 지연을 느끼지 못합니다. 그러나 이벤트가 처리되는 시점과 해당 뷰가 다시 그려지는 시점 사이에는 간격이 있기 때문에 런 루프 중 특정 지점에서 원하는 방식으로 뷰가 업데이트되지 않을 수 있습니다. 뷰의 최신 콘텐츠나 레이아웃에 의존하는 계산이 있는 경우 뷰에 대한 오래된 정보를 사용하여 작업할 위험이 있습니다. 실행 루프, 업데이트 주기 및 특정 UIView 메소드를 이해하면 이러한 종류의 문제를 피하거나 디버깅하는 데 도움이 될 수 있습니다.
아래 다이어그램에서 런 루프가 끝날 때 업데이트 주기가 어떻게 발생하는지 확인할 수 있습니다.
Layout
뷰의 레이아웃은 화면에서의 크기와 위치를 나타냅니다. 모든 뷰에는 슈퍼뷰의 좌표계에서 존재하는 위치와 크기를 정의하는 프레임이 있습니다. UIView는 뷰의 레이아웃이 변경되었음을 시스템에 알리는 방법과 뷰의 레이아웃이 다시 계산된 후 수행할 작업을 정의하기 위해 재정의할 수 있는 메서드를 제공합니다.
- layoutSubviews()
이 UIView메서드는 뷰와 모든 하위 뷰의 위치 변경 및 크기 조정을 처리합니다. 현재 뷰와 모든 하위 뷰에 위치와 크기를 제공합니다. 이 메서드는 뷰의 모든 하위 뷰에 대해 작동하고 해당 뷰들의 layoutSubviews메서드를 호출하기 때문에 비용이 많이 듭니다. 시스템은 뷰의 프레임을 다시 계산해야 할 때마다 이 메서드를 호출하므로 프레임을 설정하고 위치 및 크기를 지정하려는 경우 이 메서드를 재정의해야 합니다. 그러나 뷰 계층 구조에 레이아웃 새로 고침이 필요한 경우 이를 명시적으로 호출해서는 안 됩니다. 대신, 자체 layoutSubviews호출보다 훨씬 저렴한 런 루프 중 여러 지점에서 layoutSubviews호출을 트리거하는 데 사용할 수 있는 여러 메커니즘이 있습니다.
자동 새로고침 트리거
뷰의 레이아웃이 변경된 것을 자동으로 표시하는 여러 이벤트가 있으므로 개발자가 수동으로 이 작업을 수행하지 않고도 자동으로 다음기회(업데이트 주기에) layoutSubviews이 호출됩니다.
뷰의 레이아웃이 변경되었음을 시스템에 자동으로 알리는 몇 가지 방법은 다음과 같습니다.
- 뷰 크기 조정
- 하위 뷰 추가
- 사용자가 UIScrollView를 스크롤하는 경우( UIScrollView이나 상위 뷰에서 layoutSubviews호출됨)
- 사용자가 기기를 회전함
- 뷰의 제약 조건 업데이트
이들은 모두 뷰의 위치를 다시 계산해야 한다는것을 시스템에 전달하고 자동으로 최종 layoutSubviews 호출로 이어집니다. 그러나 layoutSubViews를 직접 실행하는 방법도 있습니다.
- setNeedsLayout()
layoutSubviews를 호출하는 비용이 가장 저렴한 방식은 setNeedsLayout을 뷰에 호출하는것 입니다. 이것은 뷰의 레이아웃을 다시 계산해야 한다는것을 시스템에 나타내는 것입니다. setNeedsLayout은 즉시 실행되어 반환되고, 반환되기 전까지 뷰를 업데이트하지 않습니다. 대신 시스템이 해당 뷰에서 layoutSubViews를 호출하고 모든 하위뷰에서 후속된 layoutSubViews를 실행할 때 다음 업데이트 주기에 뷰가 업데이트 됩니다.setNeedsLayout이 반환되는 시점과 뷰가 다시 그려지고 레이아웃되는 시점 사이에 임의의 시간 간격이 있더라도 애플리케이션에 지연을 일으킬 만큼 길어서는 안 되기 때문에 지연으로 인해 사용자에게 영향이 없어야 합니다.
- layoutIfNeeded()
layoutIfNeeded는 layoutSubviews 호출을 실행하는 UIView의 또 다른 메서드입니다. 그러나 다음 업데이트 주기에 실행하기 위해 layoutSubviews를 대기열에 넣는 대신, 뷰에 레이아웃 업데이트가 필요한 경우 시스템은 즉시 layoutSubviews를 호출합니다. setNeedsLayout을 호출한 후 또는 위에 설명된 자동 새로 고침 트리거 중 하나를 수행한 후 layoutIfNeeded를 호출하면 view에서 layoutSubviews가 호출됩니다. 그러나 layoutIfNeeded를 호출하고 뷰를 새로 고쳐야 함을 시스템에 나타내는 작업이 없으면layoutSubviews가 호출되지 않습니다. 동일한 실행 루프 동안 레이아웃을 업데이트하지 않고 뷰에 대해 두 번 layoutIfNeeded를 호출하는 경우 두 번째 호출은 layoutSubviews 호출을 실행하지 않습니다.
setNeedsLayout과 달리, layoutIfNeeded를 사용하면 하위 뷰 레이아웃 및 다시 그리기가 즉시 발생하고 이 메서드가 반환되기 전에 완료됩니다(비행 애니메이션이 있는 경우 제외). 이 방법은 새 레이아웃을 사용해야 하고 다음 업데이트 주기에 뷰가 업데이트될 때까지 기다릴 수 없는 경우에 유용합니다. 그러나 그렇지 않은 경우에는 setNeedsLayout을 대신 호출하고 다음 업데이트 주기를 기다려야 런 루프당 한 번만 뷰를 업데이트할 수 있습니다.
이 방법은 Constaints에 대한 변경 사항을 애니메이션으로 표시할 때 특히 유용합니다. 애니메이션이 시작되기 전에 모든 레이아웃 업데이트가 전파되도록 하려면 애니메이션 블록이 시작되기 전에 layoutIfNeeded를 호출해야 합니다. 새로운 Constraints를 구성한 다음 애니메이션 블록 내에서 layoutIfNeeded를 다시 호출하여 새 상태로 애니메이션을 적용합니다.
Display
뷰의 Display에는 색상, 텍스트, 이미지 및 Core Graphics 드로잉을 포함하여 뷰와 해당 하위 뷰의 크기 및 위치 지정과 관련되지 않은 뷰의 속성이 포함됩니다. Display 단계에는 업데이트를 실행하기 위한 Layout 단계와 유사한 메서드가 포함되어 있습니다. 둘 다 시스템에서 변경 사항을 감지했을 때 호출하는 메서드와 새로 고침을 실행하기 위해 수동으로 호출할 수 있는 메서드입니다.
- draw(_:)
UIView draw(Objective-C의 drawRect) 메서드는 뷰의 크기 및 위치 지정에 대해 layoutSubviews가 수행하는 것처럼 뷰의 Contents에 대해 작동합니다. 그러나 하위 뷰에서는 후속 draw 호출을 실행하지 않습니다. layoutSubviews와 마찬가지로 draw를 직접 호출해서는 안 되며 대신 실행 루프 중 여러 지점에서 draw 호출을 실행하는 메서드를 호출해야 합니다.
- setNeedsDisplay()
이 메소드는 setNeedsLayout과 동일한 표시입니다. 뷰에 콘텐츠 업데이트가 있었지만 실제로 뷰를 다시 그리기 전에 반환된다는 내부 플래그를 설정합니다. 그런 다음 다음 업데이트 주기에 시스템은 이 플래그로 표시된 모든 뷰를 살펴보고 해당 뷰에 대해 draw를 호출합니다. 다음 업데이트 주기 동안 뷰 일부의 내용만 다시 그리려면 setNeedsDisplay를 호출하고 업데이트가 필요한 뷰 내에서 rect를 전달할 수 있습니다.
대부분의 경우 뷰의 UI 구성 요소를 업데이트하면 내부 "content updated" 플래그가 자동으로 설정되어 뷰가 "dirty"로 표시되고 명시적인 setNeedsDisplay 호출이 필요하지 않고 다음 업데이트 주기에 뷰의 콘텐츠가 다시 그려집니다. 그러나 UI 구성 요소에 직접 연결되지 않았지만 업데이트할 때마다 뷰를 다시 그려야 하는 속성이 있는 경우 didSet 속성 관찰자를 정의하고 setNeedsDisplay를 호출하여 적절한 뷰 업데이트를 실행할 수 있습니다.
때로는 속성을 설정하려면 사용자 정의 그리기를 수행해야 하며, 이 경우 draw를 재정의해야 합니다. 다음 예에서 numberOfPoints를 설정하면 시스템이 지정된 점 수를 가진 모양으로 뷰를 그리도록 실행해야 합니다. 이 경우 draw에서 사용자 정의 드로잉을 수행하고 numberOfPoints의 속성 관찰자에서 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)
}
}
}
layoutIfNeeded가 크기 조정 및 위치 지정을 즉시 수행하는 것처럼 뷰에서 즉시 콘텐츠 업데이트를 실행하는 표시 방법은 없습니다. 일반적으로 뷰를 다시 그리려면 다음 업데이트 주기까지 기다리는 것으로 충분합니다. (draw는 ifNeeded가 없다)
Constraints
Auto Layout에서 뷰를 레이아웃하고 다시 그리는 세 단계가 있습니다. 첫 번째 단계는 Constraints을 업데이트하는 것입니다. 여기서 시스템은 뷰에 필요한 모든 제약 조건을 계산하고 설정합니다. 그런 다음 레이아웃 엔진이 뷰와 하위 뷰의 프레임을 계산하고 레이아웃하는 Layout 단계가 제공됩니다. Display 단계는 주기를 완료하고 필요한 경우 draw 메서드를 호출하여 뷰의 콘텐츠를 다시 그립니다(구현된 경우).
- setNeedsUpdateConstraints()
setNeedsUpdateConstraints()를 호출하면 다음 업데이트 주기에 Constraints 업데이트가 보장됩니다. 뷰의 Constraints 중 하나가 업데이트되었음을 표시하여 updateConstraints()를 실행합니다. 이 메서드는 setNeedsDisplay() 및 setNeedsLayout()과 유사하게 작동합니다.
- updateConstraintsIfNeeded()
이 메서드는 layoutIfNeeded와 동일하지만 Auto Layout을 사용하는 뷰에 사용됩니다. "constraint update" 플래그(setNeedsUpdateConstraints 또는 validateInstrinsicContentSize에 의해 자동으로 설정될 수 있음)를 확인합니다. Constraint을 업데이트해야 한다고 결정하면 updateConstraints()를 즉시 실행하고 런 루프가 끝날 때까지 기다리지 않습니다.
- invalidateIntrinsicContentSize()
Auto Layout을 사용하는 일부 뷰에는 콘텐츠가 주어진 뷰의 자연스러운 크기인 내장 콘텐츠 크기(intrinsicContentSize) 속성이 있습니다. 뷰의 본질적인 ContentSize는 일반적으로 뷰에 포함된 요소에 대한 Constraint에 따라 결정되지만 사용자 지정 동작을 제공하기 위해 재정의될 수도 있습니다. InvalidateIntrinsicContentSize()를 호출하면 뷰의 IntrinsicContentSize가 오래되어 다음 레이아웃 단계에서 다시 계산되어야 함을 나타내는 플래그가 설정됩니다.
이것들을 어떻게 다 연결할까?
뷰의 Layout, Display 및 Constraint은 업데이트되는 방식과 실행 루프 중 여러 지점에서 업데이트를 강제하는 방법에서 매우 유사한 패턴을 따릅니다. 각 구성 요소에는 실제로 업데이트를 전파하는 메서드(layoutSubviews, draw 및 updateConstraints)가 있습니다. 이를 재정의하여 뷰를 수동으로 조작할 수 있지만 어떤 상황에서도 명시적으로 호출해서는 안 됩니다. 이 메소드는 뷰의 일부 구성요소를 업데이트해야 함을 시스템에 알려주는 플래그 세트가 뷰에 있는 경우에만 런 루프가 끝날 때 호출됩니다. 이 플래그를 자동으로 설정하는 특정 작업이 있지만 명시적으로 설정할 수 있는 메서드도 있습니다. Layout 및 Constraint 관련 업데이트의 경우 이러한 업데이트에 대한 런 루프가 끝날 때까지 기다릴 수 없는 경우(즉, 다른 작업이 보기의 새 레이아웃에 종속되는 경우) 즉시 업데이트를 트리거하기 위해 호출할 수 있는 메서드가 있고, "layout updated” 플래그가 설정됩니다.
다음은 업데이트가 필요할 수 있는 UI의 각 구성 요소와 관련된 각 메서드를 간략하게 설명하는 차트입니다.
메서드 목적 | Layout | Display | Constraints |
업데이트 구현 (재정의, 명시적 호출 X ) |
layoutSubviews | draw | updateConstraints |
다음 업데이트 주기에 업데이트 표시 |
setNeedsLayout | setNeedsDisplay | setNeedsUpdateConstraints invalidateIntrinsicContentSize |
뷰가 "dirty"로 표시되면 즉시 업데이트 |
layoutIfNeeded | updateConstraintsIfNeeded | |
암시적으로 뷰가 업데이트 되도록 함 |
addSubview (뷰의 경계를 변경하는 setFrame) |
뷰의 bounds를 변경 | Constraints 활성화/ 비활성화 Constraints 값 , 우선순위 변경 뷰 계층에서 뷰 제거 |
다음 차트는 업데이트 주기와 이벤트 루프 사이의 상호 작용을 요약하고, 위에서 설명한 일부 메서드가 주기 동안 어디에 속하는지를 나타냅니다. 런 루프의 어느 시점에서든 명시적으로 layoutIfNeeded 또는 updateConstraintsIfNeeded를 호출할 수 있습니다. 이는 잠재적으로 비용이 많이 든다는 점을 명심하세요. 루프의 끝에는 특정 "update constraints", "update layout" 또는 "needs display" 플래그가 설정된 경우 Constraints, Layout 및 Display를 업데이트하는 업데이트 주기가 있습니다. 업데이트가 완료되면 런 루프가 다시 시작됩니다.
이 요약 차트와 표, 그리고 위의 보다 세부적인 메서드 설명을 통해 이러한 메서드의 사용법과 각 메서드가 기본 iOS 실행 루프와 어떻게 관련되는지 명확히 알 수 있기를 바랍니다. 이러한 방법과 뷰에서 올바른 업데이트를 효율적으로 트리거하는 방법을 이해하면 오래된 레이아웃이나 콘텐츠 및 기타 예기치 않은 동작과 관련된 문제를 방지하고 발생하는 모든 문제를 디버깅할 수 있습니다.
※구글번역의 도움을 받았지만 오역이나 잘못된 정보가 있다면 제보 부탁드립니다 (_ _)
Reference:
https://jeonyeohun.tistory.com/336
https://tech.gc.com/demystifying-ios-layout/
https://medium.com/@Alpaca_iOSStudy/viewcontroller-view-lifecycle-daed5766e02b
'iOS' 카테고리의 다른 글
iOS 테이블셀 원하는 색이 안나오는경우 (0) | 2024.04.25 |
---|---|
present로 다른 뷰로 이동후 돌아올때, viewWillApear 메서드가 호출되지않는점 (0) | 2024.04.22 |
Delegate 패턴 구현중에 delgate = nil 오류 (0) | 2024.04.22 |
아이폰에 넣은 앱이 SQLite의 DB를 불러오지 못한 에러 (0) | 2024.04.14 |
SQLite3에 대하여 Swift (0) | 2024.04.13 |