본문 바로가기

UIKIT

UICollectionViewCompositionalLayout - 섹션 헤더


import UIKit

class ViewController: UIViewController {
    
    // MARK: - Varaibles
    let data = Array(1...20).map { "Item \($0)\\nDynamic content for size adjustment" }
    
    // MARK: - UI Components
    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
            // Item 정의
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                                  heightDimension: .absolute(100))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
            
            // Group 정의
            let groupSize: NSCollectionLayoutSize
            let group: NSCollectionLayoutGroup
            
            // 섹션별 레이아웃
            if sectionIndex == 0 {
                // 섹션 0: 2열 레이아웃
                groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(100))
                group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item])
            } else if sectionIndex == 1 {
                // 섹션 1: 1열 레이아웃
                groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(150))
                group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
            } else {
                // 섹션 2: 3열 레이아웃
                let tripleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0 / 3.0),
                                                            heightDimension: .absolute(100))
                let tripleItem = NSCollectionLayoutItem(layoutSize: tripleItemSize)
                tripleItem.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
                
                groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(100))
                group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [tripleItem, tripleItem, tripleItem])
            }
            
            // Section 정의
            let section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 5
            section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
            
            // Section 가로 스크롤 활성화
            if sectionIndex == 0 {
                section.orthogonalScrollingBehavior = .continuous
            }
            
            // Header 추가
            let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                    heightDimension: .absolute(50))
            let header = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerSize,
                elementKind: UICollectionView.elementKindSectionHeader,
                alignment: .top
            )
            section.boundarySupplementaryItems = [header]
            
            return section
        }
        
        layout.register(BackgroundDecorationView.self, forDecorationViewOfKind: "background")
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.dataSource = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.register(SectionHeaderView.self,
                                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                withReuseIdentifier: SectionHeaderView.reuseIdentifier)
        
        return collectionView
    }()

    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBrown
        
        view.addSubview(collectionView)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 3
    }
    
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if section == 0 {
            return 10
        } else  if section == 1{
            return 5
        } else {
            return 15
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.backgroundColor = .systemBlue
        cell.layer.cornerRadius = 10
        cell.clipsToBounds = true
        
        // 셀 텍스트 레이블 추가
        let label = UILabel(frame: cell.contentView.bounds)
        label.text = data[indexPath.item]
        label.numberOfLines = 0
        label.textAlignment = .center
        label.textColor = .white
        label.font = UIFont.boldSystemFont(ofSize: 16)
        label.sizeToFit()
        
        // 중복 방지
        for subview in cell.contentView.subviews {
            subview.removeFromSuperview()
        }
        
        cell.contentView.addSubview(label)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView,
                        viewForSupplementaryElementOfKind kind: String,
                        at indexPath: IndexPath) -> UICollectionReusableView {
        if kind == UICollectionView.elementKindSectionHeader {
            let header = collectionView.dequeueReusableSupplementaryView(
                ofKind: kind,
                withReuseIdentifier: SectionHeaderView.reuseIdentifier,
                for: indexPath
            ) as! SectionHeaderView
            header.configure(with: "Section \(indexPath.section + 1)")
            return header
        }
        return UICollectionReusableView()
    }

    
}

class BackgroundDecorationView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .lightGray // 원하는 배경색
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}

 

 

섹션 1에만 가로 스크롤 추가:

if sectionIndex == 1 {
    section.orthogonalScrollingBehavior = .continuous
}

 

그룹 크기 조정:

  • groupSize.widthDimension을 fractionalWidth(0.8)로 설정하여 화면 너비의 80%만큼 그룹이 차지하도록 변경.
  • 가로 스크롤에서는 전체 화면 너비를 넘는 그룹 크기를 가지는 것이 자연스럽습니다.

 

orthogonalScrollingBehavior의 값:

  • .continuous: 부드럽게 스크롤.
  • .paging: 페이지 단위로 스크롤.
  • .groupPaging: 그룹 단위로 스크롤.
  • .groupPagingCentered: 그룹 단위로 스크롤하며, 항상 화면 가운데에 표시.

 

🔥 item.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)

 

contentInsets의 의미

 contentInsets는 NSDirectionalEdgeInsets로 정의되며, 다음 네 가지 값을 포함합니다:

  • top: 아이템의 위쪽 여백.
  • leading: 아이템의 왼쪽 여백.
  • bottom: 아이템의 아래쪽 여백.
  • trailing: 아이템의 오른쪽 여백.

이 값들은 아이템의 컨텐츠 영역에서부터의 외부 간격을 지정합니다.

 

 

그룹 간 간격 조정

  • 섹션에서 그룹 간의 간격은 section.interGroupSpacing으로 조정합니다. 
section.interGroupSpacing = 5

 

 

 

최종 코드

import UIKit

class ViewController: UIViewController {
    
    // MARK: - Varaibles
    let data = Array(1...20).map { "Item \($0)\\nDynamic content for size adjustment" }
    
    // MARK: - UI Components
    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
            // Item 정의
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                  heightDimension: .absolute(100))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
            
            // Group 정의
            let groupSize: NSCollectionLayoutSize
            let group: NSCollectionLayoutGroup
            
            // 섹션별 레이아웃
            if sectionIndex == 0 {
                // 섹션 0: 2열 레이아웃
                groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(100))
                group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            } else if sectionIndex == 1 {
                // 섹션 1: 1열 레이아웃
                groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(100))
                group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
            } else {
                // 섹션 2: 3열 레이아웃
                let tripleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0 / 3.0),
                                                            heightDimension: .absolute(100))
                let tripleItem = NSCollectionLayoutItem(layoutSize: tripleItemSize)
                tripleItem.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
                
                groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                   heightDimension: .absolute(100))
                group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [tripleItem, tripleItem, tripleItem])
            }
            
            // Section 정의
            let section = NSCollectionLayoutSection(group: group)
            section.interGroupSpacing = 10
            section.contentInsets = NSDirectionalEdgeInsets(top: 1, leading: 1, bottom: 1, trailing: 1)
            
            // Section 가로 스크롤 활성화
            if sectionIndex == 0 {
                section.orthogonalScrollingBehavior = .groupPagingCentered
            }
            
            // Header 추가
            let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                    heightDimension: .absolute(50))
            let header = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerSize,
                elementKind: UICollectionView.elementKindSectionHeader,
                alignment: .top
            )
            section.boundarySupplementaryItems = [header]
            
            return section
        }
        
        layout.register(BackgroundDecorationView.self, forDecorationViewOfKind: "background")
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.dataSource = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.register(SectionHeaderView.self,
                                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                withReuseIdentifier: SectionHeaderView.reuseIdentifier)
        
        return collectionView
    }()

    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBrown
        
        view.addSubview(collectionView)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        
    }
}

extension ViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 3
    }
    
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if section == 0 {
            return 10
        } else  if section == 1{
            return 5
        } else {
            return 15
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.backgroundColor = .systemBlue
        cell.layer.cornerRadius = 10
        cell.clipsToBounds = true
        
        // 셀 텍스트 레이블 추가
        let label = UILabel(frame: cell.contentView.bounds)
        label.text = data[indexPath.item]
        label.numberOfLines = 0
        label.textAlignment = .center
        label.textColor = .white
        label.font = UIFont.boldSystemFont(ofSize: 16)
        label.sizeToFit()
        
        // 중복 방지
        for subview in cell.contentView.subviews {
            subview.removeFromSuperview()
        }
        
        cell.contentView.addSubview(label)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView,
                        viewForSupplementaryElementOfKind kind: String,
                        at indexPath: IndexPath) -> UICollectionReusableView {
        if kind == UICollectionView.elementKindSectionHeader {
            let header = collectionView.dequeueReusableSupplementaryView(
                ofKind: kind,
                withReuseIdentifier: SectionHeaderView.reuseIdentifier,
                for: indexPath
            ) as! SectionHeaderView
            header.configure(with: "Section \(indexPath.section + 1)")
            return header
        }
        return UICollectionReusableView()
    }

    
}

class BackgroundDecorationView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .lightGray // 원하는 배경색
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}