배민이나 당근마켓 앱을 사용해보면, 위와 같이 스크롤에 반응하는 이미지 UI를 볼 수 있습니다.
검색해보니 이러한 UI를 Sticky Header라고 하는 것 같습니다. (확실하진 않아요!!)
스크롤시에 그저 상단으로 없어지는 단순한 UI를 만들수도 있지만, 위와 같이 좀 더 유려한 반응을 추가해서 완성도 높은 앱으로 만들면 좋을 것 같습니다.
Sticky Header 구현을 목적으로 하기 때문에 최대한 간소화해서 진행하도록 할게요
예제를 진행하기 위해 필요한 컴포넌트는 CollectionView와 ImageView 입니다.
//MARK: - StickyHeaderViewController
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.showsVerticalScrollIndicator = false
cv.clipsToBounds = true
cv.backgroundColor = .clear
cv.contentInsetAdjustmentBehavior = .never
cv.contentInset = .init(top: headerHeight, left: 0, bottom: 0, right: 0)
cv.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.identifier)
cv.register(MyHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: MyHeaderView.identifier)
cv.dataSource = self
cv.delegate = self
return cv
}()
private let headerImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "cat")
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
return view
}()
private let headerHeight = 250.0
예제를 진행하는데 필요한 프로퍼티를 선언해줍니다.
top contentInset을 이미지의 높이 만큼 설정해서 이미지를 마치 CollectoinView의 셀처럼 보이게 할 수 있습니다.
//MARK: - Helpers
private func layout() {
view.backgroundColor = .white
[collectionView, headerImageView].forEach(view.addSubview)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
headerImageView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.height.equalTo(headerHeight)
}
}
AutoLayout 설정을 해줍니다.
//MARK: - UICollectionViewDelegateFlowLayout
extension StickyHeaderViewController: UICollectionViewDelegateFlowLayout {
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
return .init(width: UIScreen.main.bounds.width, height: 150)
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int
) -> CGFloat {
return 4
}
}
FlowLayout 설정입니다.
extension StickyHeaderViewController: UICollectionViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let distanceFromOrigin = abs(scrollView.contentOffset.y) // 스크롤뷰의 원점에서 현재 스크롤 offset.y 의 거리
let scrollUp = scrollView.contentOffset.y <= -headerHeight // 이미 스크롤이 원점인 상태에서 위로 스크롤 중인가?
let stopExpandHeader = scrollView.contentOffset.y <= -(headerHeight*2) // 이미지 확장을 멈춰야 하는가?
print(scrollView.contentOffset.y)
if !stopExpandHeader, scrollUp {
// 이미지 확장 가능하고, 이미 스크롤이 원점인 상태에서 위로 스크롤 중
headerImageView.snp.updateConstraints { make in
make.height.equalTo(distanceFromOrigin)
}
headerImageView.alpha = 1
}
else if !scrollUp {
// 아래로 스크롤 중
let height = scrollView.contentOffset.y <= 0 ? distanceFromOrigin : 0
headerImageView.snp.updateConstraints { make in
make.height.equalTo(height)
}
headerImageView.alpha = distanceFromOrigin / headerHeight
}
}
}
scrollViewDidScroll 메서드를 구현하기 위해서 delegate를 채택합니다.
스크롤시 파라미터로 전달되는 scrollView의 offset을 활용해서 이미지의 높이와 투명도를 변경합니다.
따로 설명이 필요하진 않을 것 같습니다. 저도 직접 offset 값을 콘솔에 출력하면서 하나씩 구현했습니다.