관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - Swift] tableView, collectionView 스크롤 시 상단 뷰 흐리게 하는 방법 (네이버 웹툰 상단 뷰, Sticky Header) 본문

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
    }
  }
}

* 전체 코드: https://github.com/JK0369/ExHeaderScrollAnimation

Comments