[Swift] 특정 IndexPath에 있는 collectionViewCell을 StickyHeader로 만들기

Make UICollectioViewCell StickyHeader using UICollectionViewFlowLayout

naljin
9 min readApr 23, 2021

요구 사항

collectionView에서 특정 indexPath에 있는 cell을 sticky header 로 만들어야 했슴다. 바로 이렇게 말이죠.

stickyIndexPath를 IndexPath(row: 4, section: 0)로 설정 -> 분홍색 cell이 sticky Header 처럼 작동

CollectionReusableView 를 이용해서 비슷하게 header 느낌을 낼 수 있다지만 내가 원하는건 UICollectionViewCell을 sticky 하게 만드는 것인걸요..??

그렇다면 UICollectionViewFlowLayout 을 잘 조져서 커스텀해야죠 머.. 시작해봅시당. (설명이고 뭐고 다 됐고 코드만 보고 싶다면? 여기로!)

시작하기 with Starter Project

일단 이곳(starter branch)에서 간단히 작업해 놓은 collectionView 를 확인할 수 있습니다. 랜덤한 색상과 높이 (범위 — 10 ~ 109) 가 설정된 20개의 cell을 갖습니다.

Customize UICollectionViewFlowLayout

이제 UICollectionViewFlowLayout 를 건드릴 차례입니다. 이게 무엇인고.. 하면

item 을 원하는 형태로 정렬하게 도와주는 layout object

라고 할 수 있는데요! 기본적으로 item, header, footer 등의 size 를 결정하기 위해 UICollectionViewDelegateFlowLayout 를 conform 하는 delegate와 상호작용합니다. 이렇게 delegate를 사용하면 레이아웃을 동적으로 조정할 수 있다구여~!

in ViewController.swift

😞 근데 난 이 정도로 안돼..!! 내가 원하는.. 좀 더 세밀한 UI가 있다구!!!

🙋🏻‍♀️ UICollectionViewFlowLayout을 상속 받아서 custom 레이아웃의 꿈을 마음껏 펼쳐보세요!

여기서부터 시작하는거예여! 🎢

collectionView는 해당 객체의 특정 메소드들을 호출하면서 레이아웃을 처리하는데요, 각각의 메소드는 item 의 위치를 ​​계산하고 필요한 기본 정보를 collectionView 에 제공 할 수 있는 기회가 됩니다.

특히 아래 세개의 과정은 layout process 에서 항상 호출되는 것들인데요, 한번 살펴보자구요

prepare()

  • 레이아웃을 업데이트 할 때마다 가장 먼저 호출
  • 필요한 경우 레이아웃 정보 제공을 위한 사전 계산 수행
  • 즉, 향후 레이아웃 작업을 준비 할 기회 제공

collectionViewContentSize

  • 전체 콘텐츠 영역크기 반환

layoutAttributesForElements(in:)

  • 지정된 사각형에 있는 모든 셀 및 뷰의 레이아웃 특성을 반환
  • 레이아웃 프로세스의 마지막 단계에서 호출

저는 여기서 마지막으로 소개한 layoutAttributesForElements(in:)메소드만 override해서 사용할거예요! 로직은 super호출해서 나온 attributes 배열에 stickyAttribute 만 개인적으로 계산해서 추가하는 것이랍니당. 간단하쥬?

stickyAttribute를 return 하는 getStickyAttributes(at:) 함수는 이렇게 생겼습니다.

로직을 살짝 소개해보자면

  1. sticky header가 되길 원하는 cell의 IndexPath를 인자로 받음
  2. layoutAttributesForItem(at:) 함수를 통해 해당 indexPath의 layout 속성을 deep copy (.copy()) 로 가져옴
  3. 가져온 layout attribute의 minY 값이 collectionView.contentOffset.y보다 작다면 (= 즉, sticky header cell의 minY 값보다 더 스크롤 됐다면) attribute y값 조정을 통해 collectionView의 상위에 붙어있는 것처럼 보이게함
  4. zIndex 를 1로 조정해서 해당 layout attribute의 z축을 상위로 올림. zIndex 디폴트 값은 0
  5. 이렇게 값이 조정된 attribute 객체 반환

여기까지 잘 따라오셨나여?! 👏🏻👏🏻

그렇다면 족굼만 더 집중해봅시다!! 우리에겐 override 해야할 함수가 남아있다구요~

바로 shouldInvalidateLayout(forBoundsChange:) 인데요, 여기서는 true 를 반환해줍시당. 그래야 스크롤 할 때마다 layout 이 다시 계산되거든여

마지막으로 어떤 cell 이 sticky 한지 파악하기 위해 stickyIndexPath 를 작성합니다 (사실 이미 위에서 사용했던 property 죠..?ㅎ)

didSet에 설정된 invalidateLayout() 함수는 현재 레이아웃을 무효화하고 레이아웃 업데이트를 트리거하는 역할을 한답니다. 나중에 새로운 레이아웃을 적용하고 싶다면 이 값을 바꿔주면 되겠쥬? 이렇게 stickyIndexPath만 바깥에서 따로 설정하는 상황을 대비해서 private 설정은 하지 않았습니다.

좋아여! 이제 collectionView를 정의한 곳으로 돌아가서 collectionViewLayout을 여태껏 작성한 CustomCollectionViewFlowLayout 로 설정해줍시당

let columnLayout = CustomCollectionViewFlowLayout(stickyIndexPath: stickyIndexPath)self.collectionView.collectionViewLayout = columnLayout

후 끗!!!!!!!

CustomCollectionViewFlowLayout.swift

마무리

어떻게.. 이번 포스팅도 잘 보셨나여 ?_?

사실 UICollectionViewFlowLayout 에 관한 글을 몇번 검색해보신 분들은 알겠지만, 보통 layout attribute에 대한 cache array를 만들어서 사용하는 예제가 많아여. 하지만 제 상황은 캐시를 만들어서 쓰기에는 애매한 경우라고 판단해서,, 캐시를 쓰지 않는 선에서 최선을 다해봤습니다,, ^.ㅜ

하지만 부족한 점이 많으니,, 혹시 개선 사항이 보인다면 댓글 부탁드려요!!!!!

그럼 20000!

전체 프로젝트

전체 프로젝트 내용은 아래 링크에서 확인할 수 있습니다.

참고

--

--

Responses (1)