티스토리 뷰
레이아웃 작업을하면서 메소드 정리가 필요할것 같아 작성해봅니다
요약
layoutIfNeeded
: 즉시 업데이트
setNeedsLayout
: 다음 UI *update cycle에 업데이트
들어가기전에 update cycle부터 알아보고 갑시다.
update cycle이란?
iOS는 화면을 업데이트할 때, 아래의 과정을 거칩니다
유저의 이벤트 → 레이아웃 → 디스플레이 → 렌더링
어플리케이션이 유저로부터 모든 이벤트 핸들링 코드를 수행하고 다시 *Main Run Loop로 컨트롤을 반환하는 지점 입니다.
이론적인 내용이라 이해하기 어려운데 조금더 정리해보자면
1. Event Handling
: 유저가 view를 터치하거나 버튼을 누르면
2. Layout Pass
: 레이아웃 재계산이 필요한 경우 setNeedsLayout()으로 예약하거나, layoutIfNeeded()로 즉시 적용합니다.
3. Display Pass
: 레이아웃 변경이 완료되면 화면을 다시 그립니다.
4. Rendering
: GPU가 픽셀을 렌더링해 사용자에게 보여줍니다
위의 과정을 update cycle이라고 합니다.
그럼 Main Run Loop 는 뭐죠?
Main Run Loop
Run loop는 어플리케이션의 터치와 같은 input sources를 관리하는 object에 대한 프로그래밍 인터페이스 입니다.
Run loop는 시스템에서 생성, 관리하며, 각 스레드 객체에 대한 런루프 객체 생성도 담당합니다.
즉, 입력 소스를 처리하는 이벤트 처리 루프
layoutIfNeeded()
view가 레이아웃을 즉시 업데이트 하도록 하려면 이 방법을 사용
Constraint를 이용한 애니메이션을 쓰는 상황에서 자주 사용되며,
아래 예시를 통해 사용하고 안하고 차이를 먼저 봅시다.
하단의 버튼을 눌렀을 때 가운데 blueView의 height가 10씩 증가하도록 했습니다.
UI property 코드
private lazy var blueView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemBlue
return v
}()
private lazy var increaseHeightButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemBlue
v.setTitle("increase height", for: .normal)
v.setTitleColor(.white, for: .normal)
v.addTarget(self, action: #selector(increaseHeightTapped), for: .touchUpInside)
return v
}()
private var blueViewHeightConstraint: NSLayoutConstraint!
Constraint
func setupUI() {
self.view.addSubview(self.blueView)
self.view.addSubview(self.increaseHeightButton)
self.blueViewHeightConstraint = self.blueView.heightAnchor.constraint(equalToConstant: 100)
NSLayoutConstraint.activate([
self.blueView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.blueView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
self.blueView.widthAnchor.constraint(equalToConstant: 100),
self.blueViewHeightConstraint
])
NSLayoutConstraint.activate([
self.increaseHeightButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 32),
self.increaseHeightButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -32),
self.increaseHeightButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -32)
])
}
버튼을 누르고 blueView의 height값을 하기 위해 print를 남겨놓고 테스트를 해봅시다.
// 버튼 눌렀을 때
@objc func increaseHeightTapped() {
print("Before increasing blueView height: \(self.blueView.frame.height)")
self.blueViewHeightConstraint?.constant += 10
print("After increasing blueView height: \(self.blueView.frame.height)")
}
UI적으로는 blueView height가 변하긴 하지만 실질적으로 blueView.height 값은 바뀌기 전과 후가 같습니다.
이유는 frame은 레이아웃이 업데이트된 뒤에 변경되기 때문이에요.
내용을 좀 더 덧붙여보자면
iOS에서 constraint 변경 감지 → 자동으로 다음 화면 리프레시 때 레이아웃 다시 계산 → 화면 반영 순서로 진행이 되는데,
코드 상에선 레이아웃이 아직 반영되지 않았기 때문에 frame 값이 변하지 않은것 처럼 보이는겁니다.
이제 layoutIfNeeded()를 height 값을 바꾼후 적용해봅시다.
// 버튼 눌렀을 때
@objc func increaseHeightTapped() {
print("Before increasing blueView height: \(self.blueView.frame.height)")
self.blueViewHeightConstraint?.constant += 10
self.view.layoutIfNeeded()
print("After increasing blueView height: \(self.blueView.frame.height)")
}
layoutIfNeeded가 실행되면서 레이아웃을 즉시 업데이트하기 때문에 After increasing blueView height도 바뀐 frame값이 적용된걸 볼 수 있습니다.
그리고 layoutIfNeeded가 필요한 이유를 보려면 애니메이션을 구현해보면 알게 됩니다.
아래는 layoutIfNeeded가 없는 상태에서 애니메이션을 적용했을 때 입니다.
@objc func increaseHeightTapped() {
print("Before increasing blueView height: \(self.blueView.frame.height)")
UIView.animate(withDuration: 1) {
self.blueViewHeightConstraint?.constant += 10
}
print("After increasing blueView height: \(self.blueView.frame.height)")
}
결과는?
frame값도 변하지 않고 애니메이션도 적용되지 않는 모습입니다.
다음은 layoutIfNeeded적용 코드입니다.
@objc func increaseHeightTapped() {
print("Before increasing blueView height: \(self.blueView.frame.height)")
UIView.animate(withDuration: 1) {
self.blueViewHeightConstraint?.constant += 10
self.view.layoutIfNeeded()
}
print("After increasing blueView height: \(self.blueView.frame.height)")
}
늘어나는게 보이시죠?
layoutIfNeeded()가 없으면 constraint 변경은 예약만 되고, 애니메이션 없이 다음 Run Loop 에서 즉시 반영됩니다.
그래서 애니메이션 효과가 없이 보이게 되는거죠.
layoutIfNeeded()를 사용하면 애니메이션 블록 내에서 즉시 레이아웃을 업데이트해 초기/최종 상태가 감지되면서 부드러운 애니메이션이 적용됩니다.
setNeedsLayout()
layoutSubviews를 가장 적은 부하로 호출할 수 있는 메소드 입니다.
비용이 가장 적게 드는 방법으로 해당 메소드를 호출한 view는 재계산 되어야 하는 View로 인식되며 update cycle에서 해당 view의 layoutSubviews()가 호출되게 됩니다.
대부분 iOS가 알아서 setNeedsLayout을 호출하기 때문에 직접 호출할 필요는 없지만
아래의 경우에서 유용하게 사용됩니다.
constraint를 건들지 않고, view 속성을 직접 변경하는 경우
// constraint를 건들지 않은 경우
self.blueView.frame.size.height += 10
self.view.setNeedsLayout()
여러 레이아웃 변경을 한 번에 처리하고 싶을 때
self.blueViewHeightConstraint?.constant += 10
self.redViewHeightConstraint?.constant += 20
self.view.setNeedsLayout() // 한 번만 호출해서 최적화
부모 뷰 전체가 아닌, 특정 subview에만 레이아웃 갱신을 하고 싶을 때
self.blueView.setNeedsLayout()
정리
layoutIfNeeded 는 레이아웃을 즉시 업데이트하고 애니메이션, 변경 직후 상태를 바로 확인할 때 사용을 하며, 즉시 반영이기 때문에 성능 저하 우려가 있고,
setNeedsLayout 은 모든 레이아웃 업데이트를 하나의 업데이트 주기로 통합할 수 있으므로 성능적으로 이점이 있다.
그동안 기존 코드에 뒤 두 메소드의 역할이 어떤것이었는지 궁금했는데
이번기회에 동작 방식을 알고 가니 어떤 상황에 사용되야할지 명확해진것 같습니다.
참고
1. https://www.youtube.com/watch?v=0loU4SyTfho
2. https://frouo.com/posts/ios-layoutifneeded-vs-setneedslayout
3. https://duwjdtn11.tistory.com/619
4. https://skytitan.tistory.com/514
'iOS' 카테고리의 다른 글
[Swift] POP(Protocol Oriented Programming) 한번쯤 들어봤을거고, 어쩌면 나도 모르게 하고 있었던거 (0) | 2025.02.01 |
---|---|
[iOS] Decode 한번 뜯어보자 (feat. container, nestedContainer, decode) (0) | 2025.01.18 |
[iOS] UITableView cell LifeCycle (0) | 2024.12.22 |
[iOS] URLSession image upload (0) | 2024.11.24 |
[iOS] Tuist App Extension (feat. WidgetExtension) (0) | 2024.10.25 |
- Total
- Today
- Yesterday
- Swift init
- Swift joined
- swift protocol
- Swift 프로그래머스
- CS 네트워크
- Swift
- Swift Error Handling
- Swift Leetcode
- swift programmers
- Swift RIBs
- swift reduce
- removeLast()
- Swift final
- RTCCameraVideoCapturer
- 2023년 회고
- RIBs tutorial
- Combine: Asynchronous Programming with Swift
- swift 고차함수
- 원티드 프리온보딩
- Class
- ios
- Swift inout
- Swift 프로퍼티
- Swift joined()
- Swift 알고리즘
- iOS error
- Swift ModernRIBs
- swift property
- Swift 내림차순
- swift (programmers)
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |