UI 컴포넌트 (swift)
[iOS - Swift] tableView, collectionView 스크롤 시 상단 뷰 흐리게 하는 방법 (네이버 웹툰 상단 뷰, Sticky Header)
jake-kim
2022. 11. 27. 22:33

구현 아이디어
- 상단에는 UIImageView, 하단에는 스크롤되는 UITableView나 UICollectionView 준비 (예제에서는 UICollectionView 사용)
- UIImageView와 UICollectionView 레이아웃
- UICollectionView의 topAnchor를 화면의 최상단으로 제약
- UICollectionView의 top contentInset값을 UIImageView의 크기만큼 설정 - UIImageView가 마치 collectionView의 하나의 셀처럼 보이도록 하기 위함
- 상단의 UIImageView도 마치 스크롤 되는 동작처럼 보여야하므로, scrollViewDidScroll(_:) 델리게이트에서, scrollView.contentOffset.y값을 이용하여 UIImageView의 height constraint 업데이트
구현 방법
- 두 가지 뷰 준비
- collectionView에서는 indicator를 숨김 (indicator의 시작점이 UIImageView에 있는 곳에 있을것이므로)
- collectionView의 top contentInset을 상단 높이 headerHeight 만큼 설정 (상단의 UIImageView도 마치 하나의 셀처럼 동작하게끔 하기 위함)
- UIImageView의 contentMode를 .scaleAspectFill로 설정 (높이를 변경해줄건데, 높이에 따라 너비도 비율에 맞게 달라지도록 하기 위함)
// ViewController.swift
// MARK: Constants
private enum Metric {
static let headerHeight = 250.0
}
// MARK: UI
private let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 4.0
layout.minimumInteritemSpacing = 0
layout.itemSize = .init(width: UIScreen.main.bounds.width, height: 150)
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.isScrollEnabled = true
view.showsHorizontalScrollIndicator = false
// indicator 숨기기
view.showsVerticalScrollIndicator = false
// top의 간격
view.contentInset = .init(top: Metric.headerHeight, left: 0, bottom: 0, right: 0)
view.backgroundColor = .clear
view.clipsToBounds = true
view.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.id)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let headerImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "image")
// contentMode를 .scaleAspectFill로
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
- 레이아웃
- headerHeightConstraint 준비 - 스크롤 될떄마다 UIImageView의 height값을 변경해주어야 하므로 따로 선언
// MARK: Properties
private var headerHeightConstraint: NSLayoutConstraint?
// MARK: View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
[collectionView, headerImageView]
.forEach(view.addSubview)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
headerHeightConstraint = headerImageView.heightAnchor.constraint(equalToConstant: Metric.headerHeight)
headerHeightConstraint?.isActive = true
NSLayoutConstraint.activate([
headerImageView.topAnchor.constraint(equalTo: view.topAnchor),
headerImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
collectionView.dataSource = self
collectionView.delegate = self
}
- 스크롤했을때 상단의 UIImageView가 사라지거나, 다시 보이게끔 구현 방법
- scrollView.contentOffset.y 값을 사용하여 구현
- scrollView.contentOffset.y 값의 의미
- scrollView가 최상단에 닿으면 0
- scrollView의 시작점이 최상단보다 밑에 있으면 -
- scrollView의 시작점이 최상단보다 위에 있으면 +
- 3가지 상태가 존재
- 1) 초기 상태: UIImageView가 지정한 크기만큼 커졌고, 스크롤뷰의 시작점이 최상단보다 아래 존재
- 2) 스크롤 뷰의 시작점이 최상단보다 위에 존재
- 3) 스크롤 뷰의 시작점이 최상단보다 밑에 있고, 스크롤뷰 상단 contentInset이 미리 지정한 UIImageView 높이인, Metric.headerHeight보다 큰 경우
extension ViewController: UICollectionViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let constraint = headerHeightConstraint else { return }
// print(scrollView.contentOffset.y)
let remainingTopSpacing = abs(scrollView.contentOffset.y)
let lowerThanTop = scrollView.contentOffset.y < 0
let stopExpandHeaderHeight = scrollView.contentOffset.y > -Metric.headerHeight
if stopExpandHeaderHeight, lowerThanTop {
// 1) 초기 상태: UIImageView가 지정한 크기만큼 커졌고, 스크롤뷰의 시작점이 최상단보다 아래 존재
collectionView.contentInset = .init(top: remainingTopSpacing, left: 0, bottom: 0, right: 0)
constraint.constant = remainingTopSpacing
headerImageView.alpha = remainingTopSpacing / Metric.headerHeight
view.layoutIfNeeded()
} else if !lowerThanTop {
// 2) 스크롤 뷰의 시작점이 최상단보다 위에 존재
collectionView.contentInset = .zero
constraint.constant = 0
headerImageView.alpha = 0
} else {
// 3) 스크롤 뷰의 시작점이 최상단보다 밑에 있고, 스크롤뷰 상단 contentInset이 미리 지정한 UIImageView 높이인, Metric.headerHeight보다 큰 경우
constraint.constant = remainingTopSpacing
headerImageView.alpha = 1
}
}
}