기존 구현된 헤더뷰

기존 구현한 헤더뷰의 모습입니다.
스크롤이 위에 있을 때는 큰 헤더뷰를 보이다가, 스크롤을 내리면 헤더뷰를 고정하고 상단바처럼 동작하게 했습니다.
개선 목표
1) 헤더뷰 변경 위치 정확하게 구하기
사용자의 태그가 있을 때랑 없을 때 헤더뷰의 크기가 다릅니다.
이는 정확히 언제 헤더뷰가 고정상태로 변경되어야 하는지에 대한 정보가 가변적이라는 것을 의미합니다.


기존의 방식은, 특정 높이(고정값) 만큼 스크롤을 했을 때 헤더뷰의 상태를 바꾸는 방식으로 구현했는데, 이를 새로운 방식을 사용하거나 기존 방식을 수정해서 "가변적인 헤더 상태 변경 위치"를 정확히 잡는 것이 목표입니다.
위에 gif를 보면, 헤더뷰 상태가 변경될 때 셀들과 헤더 사이의 간격이 생기는 문제가 있습니다.
상태 변경 높이가 잘못 설정된 탓인데, 태그가 있는 경우에는 그 간격이 없습니다.
즉, 가변적인 헤더 상태 변경 위치를 찾아서 태그가 있던 없던, 기능 추가로 인해 높이가 바뀌던 말던 같은 동작을 보여주어야 합니다.
2) 성능 개선하기
헤더뷰의 상태가 변경되었을 때, 기존에는 관련된 모든 뷰의 제약사항을 remake하는 방식으로 구현되었습니다.
이는 곧 오버헤드의 가능성을 의미합니다. 불변적인 제약사항또한 모두 삭제되고 다시 만들어지기 때문입니다.
따라서, 변경되는 제약사항만 반영해 오버헤드를 줄이는 것이 두 번째 목표입니다.
다음 포스팅에 ,,
리팩토링: 헤더뷰 변경 위치 정확하게 구하기
1) 아이디어
두 가지 방법이 후보입니다.
첫 번째는 태그가 있는 경우, 없는 경우를 고려해서 헤더 상태 변경 위치를 계산하는 것입니다.
두 번째는 헤더 높이와 관계 없이 헤더의 바닥이 특정 위치에 걸리면 헤더 상태를 바꾸는 것입니다.
두 방식 모두 성능상 차이는 거의 없을 것입니다. 다만, 유지보수 관점에서는 이야기가 다릅니다.
첫 번째 경우를 선택한다면, 태그 이외에 다른 뷰가 생겨서 헤더의 높이가 변경되면 추가 개발이 필요합니다.
하지만 두 번째의 경우, 다른 뷰가 추가되더라도 같은 동작을 기대할 수 있습니다.
따라서, 두 번째 방법을 선택하겠습니다.
2) 기존 구현 코드 주석처리 및 수정
일단, 헷갈리지 않게 header top inset을 0으로 해줍니다.

202+6+12+12 .. 이건 뭘까.. 무슨 의도로 쓴걸까 ..
사실 전 알지만, 누군가 이걸 본다면 이런 생각을 할 것입니다. 저 숫자들의 의미는 뭘까, 왜 한번에 안쓰고 +로 썼을까
헤더 뷰의 bottom margin도 없애줍니다.
이후, 헤더 상태 변경 관련 코드를 주석처리 해줍니다.

그리고 디버깅하기 쉽도록, 헤더에 stroke를 만들어줍니다.

3) [ 헤더 바닥 ~ 디바이스 상단 ] 거리 구하는 아이디어
여기가 핵심 포인트입니다. 어떻게 하면 카테고리와 디바이스 상단의 거리를 알 수 있을까요?
frame은 superView를 좌표계로 설정하고 superView에서의 자신의 위치를 나타냅니다.
따라서, 이 superView를 자신을 1차적으로 포함하는 superView(제 경우 CollectionView)이 아닌, window로 설정해서 계산하면 됩니다.
4) getHeaderBottomPositionFromWindow 함수 구현
private func getHeaderBottomPositionFromWindow() -> CGFloat? {
guard let window = self.view.window else { return nil }
guard let header = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyPageHeaderView else { return nil }
let frame = window.convert(header.frame, from: collectionView)
return frame.maxY
}
5) 스크롤 내릴 때 헤더뷰 바꾸기
이제 특정 위치에 위치하게 되었을 때 헤더뷰를 바꿔주면 됩니다.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10) // 상단 스크롤 방지
let headerBottomPositionFromWindow = getHeaderBottomPositionFromWindow()
guard let headerBottomPositionFromWindow else { return }
if !isHeaderSticky && headerBottomPositionFromWindow <= 195 {
isHeaderSticky = true
guard let header = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyPageHeaderView else { return }
updateHeaderView(with: header)
collectionViewLayout.sectionHeadersPinToVisibleBounds = true // 상단 고정
collectionView.collectionViewLayout.invalidateLayout() // 뷰 업데이트
}

6) 중간 정리 및 스크롤 올릴 때의 아이디어
지금까지 적용된 아이디어를 그림으로 표현하면 다음과 같습니다.

스크롤을 올릴 때의 아이디어는 다음과 같습니다.
6-1) 위 그림에서 3번째 뷰를 보면 헤더가 고정되고 높이또한 고정값으로 바뀝니다. 따라서 스크롤을 올릴 때는 헤더의 바닥을 기준으로 상태 변경을 할 수 없습니다.
6-2) 따라서 스크롤을 내리면서 헤더 상태가 변경될 때의 scrollHeight를 저장하고, 스크롤을 올리면서 상태를 변경시킬 때 이 scrollHeight를 기준으로 변경합니다.
7) 스크롤 올릴 때 헤더 상태 변경 구현 및 inset 설정
private let collectionViewLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = Constants.CollectionView.itemSpacing
layout.minimumLineSpacing = Constants.CollectionView.lineSpacing
layout.sectionInset = UIEdgeInsets(top: 18, left: 24, bottom: 24, right: 24)
return layout
}()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10) // 상단 스크롤 방지
let headerBottomPositionFromWindow = getHeaderBottomPositionFromWindow()
guard let headerBottomPositionFromWindow else { return }
let scrollHeight = scrollView.contentOffset.y
if !isHeaderSticky && headerBottomPositionFromWindow <= 195 {
isHeaderSticky = true
guard let header = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyPageHeaderView else { return }
updateHeaderView(with: header)
collectionViewLayout.sectionHeadersPinToVisibleBounds = true
transitionScrollHeight = scrollHeight
collectionViewLayout.sectionInset = UIEdgeInsets(top: scrollHeight + 12, left: 24, bottom: 24, right: 24)
collectionView.collectionViewLayout.invalidateLayout()
return
}
if let transitionScrollHeight, isHeaderSticky, scrollHeight <= transitionScrollHeight {
isHeaderSticky = false
guard let header = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyPageHeaderView else { return }
updateHeaderView(with: header)
collectionViewLayout.sectionHeadersPinToVisibleBounds = false
collectionViewLayout.sectionInset = UIEdgeInsets(top: 18, left: 24, bottom: 24, right: 24)
collectionView.collectionViewLayout.invalidateLayout()
}
}
8) 결과


이제 태그 있을 때, 없을 때 상관 없이 같은 동작을 보장합니다.
'iOS > Refactoring' 카테고리의 다른 글
| [UIKit/Refactoring] 홈 화면 UX 개선하기 (리사이징, 메모리 캐싱) (0) | 2024.07.27 |
|---|---|
| [UIKit/Refactoring] MyPageProfileEditViewModel 코드 개선하기 (2) (0) | 2024.07.17 |
| [UIKit/Refactoring] MyPageProfileEditViewModel 코드 개선하기 (1) (0) | 2024.07.13 |
| [UIKit/Refactoring] MyPageNotificationViewModel 코드 개선하기 (0) | 2024.07.09 |
| [UIKit/Refactoring] getRelative... 코드 삭제 + 관련 문제 해결 (0) | 2024.06.24 |