- Autolayout 모든 것: 사이클부터 제약조건까지2025년 03월 26일
- 2료일
- 작성자
- 2025.03.26.:17
오토레이아웃이란?
iOS 및 macOS에서 UI요소의 위치와 크기를 동적으로 관리하는 시스템
: SuperView 크기가 변경되면, Constraints로 잡혀있는 값을 기준으로 본인의 크기를 적절하게 변화시키는 것입니다.
레이아웃 업데이트 사이클
Autolayout은 크게 3가지 레이아웃 처리 과정을 통해 화면을 그립니다.
1. Update Constraints메서드가 호출되며 기존 제약 조건을 갱신하거나 새로운 제약조건을 추가하는 로직 수행
2. Layout 계산: layoutSubviews 메서드가 호출되며 업데이트된 제약 조건을 바탕으로 실제 프레임 계산
3. Display: draw 메서드 호출되어 계산된 프레임에 따라 화면이 그려진다.
이 사이클은 엄격히 순차적으로 매번 실행되는 것이 아닌 UIKit 뷰 업데이트 사이클 내에서 필요에 따라 호출됩니다.
즉, 제약조건 업데이트는 제약조건이 변경되었을때만 선택적으로 호출되고, Draw단계는 커스텀 드로잉이 필요한 경우(setNeedsDisplay 호출시)에만 실행된다.
일반적으로 이 사이클은 다음 런루프 마지막쯤에 자동으로 실행되어진다.
Automatic refresh Triggers
시스템이 자동으로 레이아웃 업데이트 필요하다고 인식하여 개발자의 요청없이도 다음 런루프에서 layoutSubviews가 호출되는 경우:
- View를 resizing
- SubView 추가
- UIScrollView 스크롤할때 그것의 부모뷰에 layoutSubviews호출
- Device 회전
- View의 Constraint변경
자동으로 시스템이 뷰의 위치가 변했고 다시 계산하여 layoutSubviews가 호출된다.
수동 레이아웃 업데이트 요청 메서드
다음 런루프에서 업데이트(비동기적)
- setNeedsUpdateConstraints: 다음 업데이트 사이클에서 제약 조건을 업데이트하도록 표시
- setNeedsLayout: 다음 업데이트 사이클에서 레이아웃을 다시 계산하도록 표시
-> 결국 이 두개는 해당 뷰에 "업데이트 필요" 플래그를 설정하는 것. 그래서 내부에 있는 needsUpdateConstraints나 needsLayout 속성을 true로 변경
-> 그러면 RunLoop에서 플래그가 설정된 뷰를 확인하고 해당 뷰에 대해 updateConstraints, layoutSubviews 호출.
즉시 업데이트(동기적)
- updateConstraintsIfNeeded: 제약 조건 업데이트가 필요한 경우 즉시 수행
- layoutIfNeeded: 레이아웃 업데이트가 필요한 경우 즉시 수행.
-> 다음 Run Loop를 기다리지 않고 현재 Run Loop에서 즉시 실행.
그렇다면 즉시나 다음 런루프에 넘기는걸 어떻게 판단하고 사용해야 할까?
SetNeedsLayout(다음 런루프에 레이아웃 업뎃이 피료해...)
1. 콘텐츠 업데이트 후 레이아웃 변경이 필요할때
func updateUserProfile() { userNameLabel.text = user.name userBioLabel.text = user.bio profileImageView.image = user.profileImage // 텍스트와 이미지가 변경되었으니 레이아웃 업데이트가 필요함을 표시 userInfoContainer.setNeedsLayout() // 이 시점에서는 실제 레이아웃 계산이 일어나지 않음 // 뷰 계층 구조의 다른 업데이트도 진행할 수 있음 updateUserStats() updateActionButtons() }
2. 여러 뷰 레이아웃을 한꺼번에 처리할때
func applyNewTheme() { // 여러 UI 요소 업데이트 headerView.backgroundColor = theme.headerColor headerView.setNeedsLayout() tableView.separatorColor = theme.separatorColor tableView.setNeedsLayout() footerView.backgroundColor = theme.footerColor footerView.setNeedsLayout() // 모든 변경 사항이 다음 업데이트 사이클에서 한 번에 적용됨 }
LayoutIfNeeded(즉시 업뎃이 피료해..)
헷갈릴 수 있는 포인트가 있다. aview.layoutIfNeeded()인 경우 aview만 업뎃되는것인가?
ㄴㄴ ~=> ? a의 서브뷰들도 업데이트 된다.
그러면 a뷰가 b뷰를 가지고 있을때 a.layoutIfNeeded() 호출하면 b.layoutIfNeeded()가 호출되는가? ㄴㄴ
a.layoutIfNeeded()를 호출해서 a뷰의 레이아웃이 업데이트되고 b입장에서 a뷰 레이아웃이 업데이트 되었으므로 자동으로 업데이트 되는것. A뷰의 레이아웃 프로세스내에서 B뷰의 레이아웃도 처리한다고 이해하면 될듯
1. 애니메이션과 함께 레이아웃 변경을 적용할때
func showDetailPanel() { // 제약 조건 변경 detailPanelHeightConstraint.constant = 300 // 애니메이션과 함께 즉시 레이아웃 변경 적용 UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() } }
2. 레이아웃 기반 계산이 필요할때
func calculateContentSize() -> CGSize { // 콘텐츠 변경 textView.text = longText // 실제 크기를 계산하기 위해 즉시 레이아웃 업데이트 textView.layoutIfNeeded() // 이제 정확한 contentSize를 얻을 수 있음 return textView.contentSize }
3. 사용자 인터랙션 중 즉각적인 UI 업뎃 필요할때
@objc func handleSliderValueChanged(_ sender: UISlider) { // 슬라이더 값에 따라 프로그레스 바 너비 변경 progressBarWidthConstraint.constant = CGFloat(sender.value) * containerView.bounds.width // 사용자 인터랙션 중이므로 즉시 변경 사항 적용 progressBar.layoutIfNeeded() }
setNeedsUpdateConstraints(다음 런루프때 제약조건 업뎃..)
1. 동적으로 제약 조건을 생성하거나 수정하는 커스텀 뷰
class DynamicConstraintView: UIView { var shouldUseCompactLayout = false { didSet { if shouldUseCompactLayout != oldValue { // 레이아웃 모드가 변경되었으므로 제약 조건 업데이트 필요 setNeedsUpdateConstraints() } } } override func updateConstraints() { // 기존 제약 조건 제거 NSLayoutConstraint.deactivate(currentConstraints) if shouldUseCompactLayout { // 좁은 레이아웃용 제약 조건 생성 및 활성화 currentConstraints = createCompactConstraints() } else { // 기본 레이아웃용 제약 조건 생성 및 활성화 currentConstraints = createRegularConstraints() } NSLayoutConstraint.activate(currentConstraints) super.updateConstraints() } }
2. 여러 뷰 간의 관계가 변경되는 경우
func updateViewHierarchy(showExtraInfo: Bool) { // 뷰 계층 구조 변경 if showExtraInfo { containerView.addSubview(extraInfoView) extraInfoView.translatesAutoresizingMaskIntoConstraints = false } else { extraInfoView.removeFromSuperview() } // 제약 조건 업데이트 필요성 표시 containerView.setNeedsUpdateConstraints() }
setNeedsDisplay
그리는 마지막 과정은 draw라고 했었다. 근데 draw method는 자식들의 draw까지는 호출하지 않는다고한다.
setNeedsDisplay는 다음 런루프때 draw를 호출해서 다시 그려지도록 한다. 만약 view의 일부분만 다시 그려지길 원하면 인자로 rect전달할수 있다.
언제 뭘 사용할지 예시
1. 키보드 표시에 따른 입력필드 위치 조정
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) @objc func keyboardWillShow(notification: NSNotification) { if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { bottomConstraint.constant = keyboardSize.height UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() // 키보드 애니메이션과 함께 뷰 위치 조정을 애니메이션화 } } }
2. 동적 셀 높이 조정
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "DynamicCell") as! DynamicCell cell.configure(with: data[indexPath.row]) // 셀 내부의 제약조건 업데이트만 하고, 실제 레이아웃은 시스템이 알아서 처리하도록 함 cell.contentLabel.text = longText cell.setNeedsLayout() return cell } // 셀 높이 계산 시 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let prototype = self.prototypeCell! prototype.configure(with: data[indexPath.row]) prototype.layoutIfNeeded() // 높이 계산을 위해 즉시 레이아웃 적용 return prototype.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height }
성능 최적화 전략
효율적인 레이아웃 관리는 앱의 성능에 큰 영향을 미칩니다. 그래서 팁!
1. 제약조건 일괄 처리
// 비효율적: 매번 레이아웃 계산 유발 가능 view1.heightAnchor.constraint(equalToConstant: 100).isActive = true view2.widthAnchor.constraint(equalToConstant: 200).isActive = true // 효율적: 한 번에 처리 NSLayoutConstraint.activate([ view1.heightAnchor.constraint(equalToConstant: 100), view2.widthAnchor.constraint(equalToConstant: 200) ])
2. 불필요한 업데이트 최소화
setNeedsLayout()이나 setNeedsUpdateConstraints() 빈번히 호출하면 성능 저하! 꼭 필요한 경우에만 호출해야한다.
IntrinsicContentSize
view가 자신의 컨텐츠를 적절하게 표시하기 필요한 크기.
ex) UILabel: 텍스트와 폰트에 따라 자연스러운 크기 계산. UIButton: 타이틀과 이미지에 따라 필요한 크기 결정.
intrinsicContentSize는 Auto Layout 시스템에 중요한 정보를 제공한다. 이 정보를 바탕으로 시스템은 명시적인 크기 제약 조건 없이도 뷰의 크기를 결정할 수 있다.
invalidateIntrinsicContentSize
뷰의 내용이 변경되어 intrinsicContentSize가 다시 계산되어야 함을 시스템에 알리는 메서드. 이 메서드를 호출하면 다음 레이아웃 업데이트 사이클 중 'Update Constraints'단계에서 intrinsicContentSize가 다시 계산된다.
class DynamicLabel: UIView { var text: String = "" { didSet { textLabel.text = text invalidateIntrinsicContentSize() } } private let textLabel = UILabel() override var intrinsicContentSize: CGSize { let labelSize = textLabel.intrinsicContentSize return CGSize(width: labelSize.width + 20, height: labelSize.height + 20) } }
성능에 미치는 영향
1. 계산 비용
intrinsicContentSize를 계산하는데 많은 리소스가 필요할 수 있습니다. 특히 텍스트 렌더링, 복잡한 그래픽 계산 혹은 재귀적인 레이아웃 계산이 포함된 경우 성능에 영향을 줄 수 있습니다. 그래서 복잡한 커스텀 뷰에서는 결과를 캐싱하는 방법이 좋을 수 있다.
2. 레이아웃 업데이트 트리거
invalidateIntirinsicContentSize()를 호출하면 레이아웃 업데이트가 필요함을 시스템에 알리게 된다. 이로 인해 레이아웃 엔진이 작동하여 제약조건을 다시 계산하게 된다. 빈번하게 하면 불필요한 레이아웃 패스를 발생시켜 성능을 저하시킬 수 있다.
3. 중첩된 뷰에서의 영향
한 뷰의 intrinsicContentSize 변경이 상위 뷰의 레이아웃에 영향을 끼칠 수 있다. 즉 전체 뷰 계층 구조의 레이아웃을 다시 계산하게 만들어 성능 저하 초래할 수 있다.
-> 레이아웃 구조 평면화: 가능한 경우 뷰 계층 구조를 단순화하여 연쇄적인 레이아웃 업데이트 최소화.
-> 명시적 크기 제약 조건 사용: 성능이 중요하면 intrinsicContentSize에 의존하는 것이 아닌 명시적인 크기 제약 조건을 사용
최적화 방법:
1. 레이아웃 구조 평면화: 가능한 경우 뷰 계층 구조 단순화하여 연쇄적인 레이아웃 업데이트를 최소화
2. 명시적 크기 제약 조건 사용: 성능이 중요한 경우 intrinsicContentSize에 의존하는 것 보다 명시적인 크기 제약 조건을 사용하는것이 좋을 수 있다
우선순위와 시스템 동작 방식
AutoLayout은 우선순위가 높은 제약 조건부터 만족시키려 처리합니다. 가장 높은 우선순위 1000은 반드시 만족되어야한다는 것을 뜻하며 그렇지 않을 경우 충돌이 발생. 우선순위가 낮은거는 필요시 무시될 수 있다고 한다.
자연스레 나오는게
Content Hugging Priority
: 뷰가 자신의 내부 콘텐츠 크기보다 더 커지는 것을 얼마나 저항하는가
- 우선순위가 높을수록: 뷰는 자신의 intrinsicContentSize보다 더 커지지 않으려고 강하게 저항
- 우선순위가 낮을수록: 뷰는 필요시 더 쉽게 확장될 수 있다.
Compression Resistance Priority
: 뷰가 자신의 내부 컨텐츠 크기보다 더 작아지는것을 얼마나 저항하는가
- 우선순위가 높을수록: 뷰는 자신의 intrinsicContentSize보다 작아지지 않으려고 더 강하게 저항
- 우선순위가 낮을수록: 뷰는 필요시 더 쉽게 압축될 수 있다.
우선순위와 관련된 디버깅 팁
1. 콘솔 확인
2. Debug View Hierarchy 사용
3. 제약 조건 로깅
'면접준비' 카테고리의 다른 글
근본으로돌아가자(7)-String,Array으로 시작해서 Sequence까지 (0) 2025.04.04 Metal(1)- 메탈을 알기전에 필요한 것들 (1) 2025.03.29 Apple의 보안 (0) 2025.03.15 근본으로 돌아가자(6) Image (2) 2025.03.05 근본으로 돌아가자(5) - 프로토콜 (0) 2025.03.03 다음글이전글이전 글이 없습니다.댓글