1. 문제 상황
홈 화면 상단에 유동적이지 않은 뷰(헤더)를 리뉴얼 전 프로젝트에선 tableHeaderView로 해결해보려 했으나 제약 경고 해결을 시간 내로 빠르게 해결하지 못할 것 같아 섹션 헤더로 넣어 해결했다. 이번 리뉴얼 버전에선 이 헤더가 사실 화면 전체의 상단 영역을 책임 진다고 생각하여 tableHeaderView로 구현해보고 싶어 시도했으나 계속 제약 조건 경고가 뜸
내부적으로 leading, trailing에 대한 제약조건 16을 달아둔 것들이 문제가 됐고 마지막 사항은 HomeHeaderContainerView라는 헤더 뷰 자체가 width가 0이라서 문제가 된다고 한다.
tableView.snp.makeConstraints {
$0.top.equalTo(topBar.snp.bottom)
$0.directionalHorizontalEdges.bottom.equalToSuperview()
}
?? 지는 분명히 HomeView의 서브뷰로 tableView를 등록했고 directionalHorizontalEdges를 통해 양끝에 맞춰뒀는디요?
func configure() {
setHierarchy()
setStyles()
setConstraints()
setTableHeaderView()
}
func setTableHeaderView() {
tableView.tableHeaderView = tableHeaderView
tableHeaderView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 332) // TODO: - 셀의 상단 그림자 영역 보장을 위해 8만큼은 넘겨줘야 함.
let fittingSize = tableHeaderView.systemLayoutSizeFitting(
UIView.layoutFittingCompressedSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
tableHeaderView.frame.size.height = fittingSize.height
tableView.tableHeaderView = tableHeaderView
}
빌드 후 메모리에 컴포넌트를 올리고 제약조건 잡고 렌더링하는 과정 속에서 시점이 꼬이는 경우가 많다. 따라서 width가 왜 0으로 잡히는지 보고자 위 setTableHeaderView에 tableView.bounds.width를 print해보았다. (tableHeaderView는 tableView의 프로퍼티로 들어가기 때문에 제약조건을 따라갈 것이므로!)
본래 setConstraints 하고 나면 테이블뷰의 제약조건이 설정되기에 이를 기반으로 사이즈를 잡을 수 있을 줄 알았으나 메모리상 완전 설정되기 전에 setTableHeaderView를 불러오는 게 아닐까 싶다.
제약조건 설정 - 설정 전 tableView의 width로 tableHeaderView 설정 - tableView의 width가 제약조건에 기반해 제대로 설정됨
아마 이 순서로 진행이 된 것 같다.
2. 문제 해결
레이아웃을 반영하라는 처리를 했으나 이 반영이 완벽히 이뤄지기 전에 tableHeaderView의 width를 잡으려했으니 문제였다. 이를 해결하기 위해선 완전히 테이블뷰의 width 제약조건까지 계산되어 반영되고 나서 이를 기반으로 width가 설정되어야 문제가 없다.
제대로된 레이아웃이 반영되고 나서 설정해야 하기 때문에 layoutSubviews를 오버라이드하는 방향을 선택했다.
override func layoutSubviews() {
super.layoutSubviews()
print("layoutSubviews - tableView bounds: \(tableView.bounds)")
setTableHeaderView()
}
// 생략....
func setTableHeaderView() {
print("setTableHeaderView - tableView bounds: \(tableView.bounds)")
// 먼저 tableHeaderView의 frame을 tableView width에 맞게 설정
tableHeaderView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 0)
tableView.tableHeaderView = tableHeaderView
}
우선 되길래 굳이 넣어야할까 싶었던 코드들을 조금씩 쳐내보니 훨씬 간단하게 써도 구현이 되었다..
이를 layoutSubviews에 넣고 실행해보았다.
다만..! 쓰면서 layoutSubviews에 대해 찾아보다보니 하위 서브뷰들에 대해 모두 이 메서드를 처리하는 것이다보니 비용이 많이 들 수 있어 가급적 쓰지 말고 setNeedsLayout / layoutIfNeeded를 사용하라고 한다.
근데 directly하게 사용하지 말라고 한다. 어느 컴포넌트 대상으로 이를 쓰게 되면 시스템이 정해둔 레이아웃 사이클을 무시한채 레이아웃을 잡도록 하기에 이 경우에 매우 큰 비용이 발생될 수 있는 것이고 override해서 쓰게 되면 시스템 상 layoutSubviews라는 메서드를 통해 레이아웃을 모두 설정하게 되고 나서의 동작을 지정해줄 수 있게 되는 것이기 때문에 해당 방식을 사용해도 되는 것 같다.
결과 코드
override func layoutSubviews() {
super.layoutSubviews()
setTableHeaderView()
}
// 생략 ....
func setTableHeaderView() {
tableHeaderView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 332)
tableView.tableHeaderView = tableHeaderView
}
레이아웃이 반영되고 관련된 로직들의 순서가 꼭 안전하게 보장되는 것만은 아니었다. 이런 경우 시점마다 실제 프레임이 어떻게 잡히고 있는지 확인해 조정해줄 필요가 있겠다. 특히나 어느 레이아웃에 의존적인 컴포넌트를 설정할땐 주의해야 할 필요가 있겠다.
+
8/1 ) tableHeaderView 내 인터렉션이 가능해야 하는 컴포넌트들에 대한 상호작용이 이뤄지지 않았는데 이는 frame 설정 시 height를 0으로 설정해둬서 그런거라 332라는 계산된 값으로 설정했다. 높이를 0으로 하면 동작이 되지 않는 것을 알았다.
'iOS > Swift' 카테고리의 다른 글
[25.08.14] MOUP 리팩토링 및 트러블슈팅 - 동적 높이를 가지는 테이블뷰 셀 (3) | 2025.08.14 |
---|---|
[25.08.04] MOUP 트러블슈팅 - .xcconfig 내 url 설정 시 주의사항 (5) | 2025.08.05 |
[25.06.24] MOUP 트러블슈팅 - Listener와 Rx의 timeout (0) | 2025.06.24 |
[TIL / 25.05.27] 날씨 앱 main 페이지 구조 트러블슈팅 (5) | 2025.05.27 |
[TIL / 25.05.18] 의존성 주입 담당 DIContainer를 처음 적용해보았습니다 (4) | 2025.05.18 |