[TIL] UIKit으로 Sticky Header 만들기

숑이·2023년 10월 3일
0

iOS

목록 보기
22/26
post-thumbnail
post-custom-banner

예제 결과물

배민이나 당근마켓 앱을 사용해보면, 위와 같이 스크롤에 반응하는 이미지 UI를 볼 수 있습니다.
검색해보니 이러한 UI를 Sticky Header라고 하는 것 같습니다. (확실하진 않아요!!)

스크롤시에 그저 상단으로 없어지는 단순한 UI를 만들수도 있지만, 위와 같이 좀 더 유려한 반응을 추가해서 완성도 높은 앱으로 만들면 좋을 것 같습니다.

Sticky Header 예제

Sticky Header 구현을 목적으로 하기 때문에 최대한 간소화해서 진행하도록 할게요

예제를 진행하기 위해 필요한 컴포넌트는 CollectionView와 ImageView 입니다.

Properties

//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의 셀처럼 보이게 할 수 있습니다.

AutoLayout

    //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 설정을 해줍니다.

FlowLayout

//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 설정입니다.

UICollectionViewDelegate

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 값을 콘솔에 출력하면서 하나씩 구현했습니다.

profile
iOS앱 개발자가 될테야
post-custom-banner

0개의 댓글