[내일배움캠프] 260205 TIL - compositional CollectionView

Bambu·2026년 2월 5일

내배캠 TIL

목록 보기
33/52

1. CollectionView

너무 힘들다 컬렉션뷰

1) compositional Layout 만들기

compositional Layout을 만들기 위한 기본 개념은 위와 같다.

item은 곧 1개의 셀이라 보면 되고, 셀의 묶음이 그룹, 그룹의 묶음이 섹션이다.

위와 같은 grid CollectionView를 만들어보자.

가. item 설정하기

func makeCompositionalLayout() -> UICollectionViewLayout {
	return UICollectionViewCompositionalLayout(sectionProvider: { section, environment in
    	let spacing: CGFloat = 10
          
          // CollectionView 사이즈 - effectiveContentSize는 인셋 빼고 계산, 그냥 contentSize는 인셋 포함하여 계산
          let containerSize = environment.container.effectiveContentSize
          
          // Item 설정
          let itemSize = (containerSize.width - spacing * 2) / 3 // 아이템 1개 가로 사이즈
          let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
              widthDimension: .absolute(itemSize),
              heightDimension: .fractionalHeight(1)
          ))

compositional layout은 UICollectionViewCompositionalLayout(sectionProvide:) 함수를 통해 생성한다.

sectionProvider 클로저는 섹션 인덱스를 뜻하는 sectionany NSCollectionLayoutEnvironment 타입의 environment를 파라미터로 받는다.

containerSize는 전체 컬렉션뷰의 크기를 받는다. 위 코드에서는 environment.container.effectiveContentSize를 사용하고 있는데, environment.container.contentSize도 존재한다. 둘의 차이점은 컬렉션뷰 자체에 양옆에 마진으로 공백을 두었을 때, 해당 공백을 크기에 포함할 것인가에 있다. efffectiveContentSize는 여백을 제외하고, contentSize는 여백을 포함한다.

itemSize는 아이템 1개, 즉 셀 1개의 가로 사이즈이다. 한 줄에 3개의 아이템을 배치할 것이니 (containerSize.width - spacing * 2) / 3으로 하나의 아이템 길이를 계산한다.

NSCollectionLayoutItem(layoutSize:) 함수로 아이템 사이즈를 정의한다. .absolute는 상수값을, .fractional은 비율을 의미한다.

나. group 설정하기

	  // Group 설정
        // 내부 그룹(아이템 2개 표시 횡렬 그룹) 설정
		let inLineGroup1 = NSCollectionLayoutGroup.horizontal(
        			layoutSize: NSCollectionLayoutSize(
        	    	widthDimension: .fractionalWidth(1),
           	 	heightDimension: .absolute(itemSize)
      	    	),
          		repeatingSubitem: item,
          		count: 3
        		)
                
        let inLineGroup2 = NSCollectionLayoutGroup.horizontal(
          layoutSize: NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .absolute(itemSize)
          ),
          repeatingSubitem: item,
          count: 3
        )
        
        // 그룹 내부 아이템 간 공백 설정
        inLineGroup1.interItemSpacing = .fixed(spacing)
        inLineGroup2.interItemSpacing = .fixed(spacing)

		// 전체 그룹 설정
        let entireGroup = NSCollectionLayoutGroup.vertical(
          layoutSize: NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .absolute(itemSize * 2 + spacing)
          ),
          subitems: [inLineGroup1, inLineGroup2]
        )
        
        // 전체 그룹 내부 그룹 간 공백 설정
        entireGroup.interItemSpacing = .fixed(10)

NSCollectionLayoutGroup.horizontal(layout: repeatingSubitem: count:) 함수로 그룹을 생성한다.

layout은 그룹의 크기를, repeatingSubitem은 그룹에서 반복하여 들어갈 아이템을, count는 그룹에서 아이템이 몇 번 반복될지를 결정한다.

지금은 3개의 아이템이 횡으로 나열된 그룹 2개가 필요하므로 inLineGroup이라 이름붙인 동일한 그룹을 2개 생성해주었다.

이후 2개의 inLineGroupentireGroup이라는 하나의 그룹으로 묶어준다.

다. section 설정하기

	  // Section 설정
        let section = NSCollectionLayoutSection(group: group)
        return section
    }
}

마지막으로 NSCollectionLayoutSection(group:)을 통해 섹션을 생성한다.

UICollectionViewCompositionalLayout(sectionProvider:) 함수에서 sectionProvider 클로저는 섹션을 반환값으로 받기 때문에 section을 리턴해주면 레이아웃이 완성된다.

전체 코드
func makeCompositionalLayout() -> UICollectionViewLayout {
	return UICollectionViewCompositionalLayout(sectionProvider: { section, environment in
  		  let spacing: CGFloat = 10
          
          // CollectionView 사이즈 - effectiveContentSize는 인셋 빼고 계산, 그냥 contentSize는 인셋 포함하여 계산
          let containerSize = environment.container.effectiveContentSize
          
          // Item 설정
          let itemSize = (containerSize.width - spacing * 2) / 3 // 아이템 1개 가로 사이즈
          let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
              widthDimension: .absolute(itemSize),
              heightDimension: .fractionalHeight(1)
          ))
          
        // Group 설정
          // 내부 그룹(아이템 2개 표시 횡렬 그룹) 설정
		  let inLineGroup1 = NSCollectionLayoutGroup.horizontal(
          			layoutSize: NSCollectionLayoutSize(
          	    	widthDimension: .fractionalWidth(1),
             	 	heightDimension: .absolute(itemSize)
      	        	),
          	    	repeatingSubitem: item,
          	    	count: 3
        	    	)
                
          let inLineGroup2 = NSCollectionLayoutGroup.horizontal(
          			layoutSize: NSCollectionLayoutSize(
          	    	widthDimension: .fractionalWidth(1),
             	 	heightDimension: .absolute(itemSize)
      	        	),
          	    	repeatingSubitem: item,
          	    	count: 3
        	    	)
        
          // 그룹 내부 아이템 간 공백 설정
          inLineGroup1.interItemSpacing = .fixed(spacing)
          inLineGroup2.interItemSpacing = .fixed(spacing)

		  // 전체 그룹 설정
          let entireGroup = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
              widthDimension: .fractionalWidth(1),
              heightDimension: .absolute(itemSize * 2 + spacing)
            ),
            subitems: [inLineGroup1, inLineGroup2]
          )
        
          // 전체 그룹 내부 그룹 간 공백 설정
          entireGroup.interItemSpacing = .fixed(10)
          
        // Section 설정
          let section = NSCollectionLayoutSection(group: group)
          return section
    }
}

2) FooterView에 값 전달하기

FooterView에 배치한 UIPageControl에서 그룹을 변경할 때마다 currentPage가 바뀌도록 하고 싶었다.

pageControl이 컬렉션뷰가 아닌 footerview에 위치하다보니 전달을 어떻게해야하나 어려웠다..

튜터님께서 footerView의 pageControl을 컬렉션뷰가 참조하도록 하면 (야매 방법이지만) 해결가능하지 않을까 귀띔해주셨다.

class GachaCollectionView {
	var pageControl: UIPageControl?
    ...
}

class ViewController {

...

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case "FooterKind":
            let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! LegendaryItemFooterView
            
            mainView.gachaCollectionView.pageControl = footerView.pageControl
            
            footerView.pageControl.numberOfPages = switch dataSource[indexPath.section] {
            case .first(let items):
                items.count / 4
            default: 0
            }
            return footerView
            
            ...

footerView를 등록하는 함수에서 컬렉션뷰의 pageControl에 footerView.pageControl을 할당하여 참조하도록 하였다.

class GachaCollectionView {
	func setLegendaryItemListSection {
    	...
		section.visibleItemsInvalidationHandler = { [weak self] _, contentOffset, environment in
              let currentPage = Int(max(0, contentOffset.x / containerSize.width)) // 현재 페이지 = 현재 스크롤 위치(x) / 컬렉션뷰 가로 길이
              self?.pageControl?.currentPage = currentPage // 현재 페이지 변경
          }
          ...
      }
  }

덕분에 스크롤이 될 때마다 현재 페이지를 footerView.pageControl로 전달하여 pageIndicator를 이동할 수 있게 되었다.

3) didSelectItemAt

UICollectionViewDelegate를 채택하여 didSelectItemAt 함수를 정의하였는데 동작이 되지 않았다.

버튼 섹션에서 동작하기를 바랬는데, 얘만 didSelectItemAt 함수가 먹지 않고 itemList 섹션에서는 함수가 동작했다.

버튼 섹션의 셀에 UIButton을 올려두었는데, 혹시 버튼이라서 didSelectItemAt 함수가 안되나? 싶어 셀의 컴포넌트를 레이블로 변경해보았다.

기존 코드

class GachaButtonCell: UICollectionViewCell {
    let button = ActionButton(title: "")
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(button)
        button.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.width.equalToSuperview()
        }
    }
}

변경 코드

class GachaButtonCell: UICollectionViewCell {
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        label.font = .systemFont(ofSize: 16, weight: .semibold)
        label.textColor = .white
        label.backgroundColor = .mushroomOrange
        
        contentView.addSubview(label)
        label.snp.makeConstraints {
            $0.center.equalToSuperview()
            $0.width.equalToSuperview()
        }
    }
}

셀에 올라간 컴포넌트를 레이블로 변경했더니 함수가 잘 동작했다.

아마 버튼이 올라갔을 경우 didSelectItemAt보다는 버튼에 설정된 액션이 우선하여 동작해서 그런 것 같다.

profile
안녕하세요, iOS 개발을 공부하고 있는 Bambu입니다. (프로필: Swifticons)

0개의 댓글