본문 바로가기
💡 Today I Learned/스터디 자료정리

[스터디9일차] CollectionView

by 솔비님 2024. 11. 26.


1.  기본적인 컬렉션뷰의 구조와 사용

class ViewController: UIViewController {

    //콜렉션뷰 선언
    var collectionView: UICollectionView!

    //데이터 배열
    let data = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]

    override func viewDidLoad() {
        super.viewDidLoad()

        //1. 콜렉션뷰 레이아웃 설정 (기본적인 그리드 레이아웃)
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 100, height: 100) //셀 크기
        layout.minimumLineSpacing = 10 //셀 사이 수직 간격
        layout.minimumInteritemSpacing = 10 //셀 사이 수평 간격

        //2. 콜렉션뷰 초기화
        collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
        collectionView.backgroundColor = .white //배경색 설정

        //3. 콜렉션뷰에 사용할 셀 등록
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")

        //4. 델리게이트 및 데이터소스 설정
        collectionView.delegate = self
        collectionView.dataSource = self

        //5. 뷰에 콜렉션뷰 추가
        self.view.addSubview(collectionView)
    }
}

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {

    //섹션당 항목 수
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }

    //셀 생성 및 재사용
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        //1. 셀 재사용
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)

        //2. 셀 초기화: 이전에 추가된 하위 뷰를 모두 제거 (재사용 시 기존 뷰가 남아있을 수 있음)
        cell.contentView.subviews.forEach { $0.removeFromSuperview() }

        //3. 셀에 데이터 추가 (라벨 추가)
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height))
        label.text = data[indexPath.row]
        label.textAlignment = .center
        label.textColor = .white

        //4. 셀에 배경색 추가
        cell.backgroundColor = .blue

        //5. 셀에 라벨 추가
        cell.contentView.addSubview(label)

        return cell
    }

    //셀 선택 처리
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let selectedItem = data[indexPath.row]
        print("\(selectedItem) 선택됨")
    }
}

 

 

 


2.  필수 프로토콜

필수 프로토콜은 데이터를 관리하고 사용자 상호작용을 처리하기  위해 사용된다
ViewController를 확장(익스텐션) 한다

 

 

2-1.  UICollectionViewDataSource

: 데이터 관리를 담당하는 메서드

 

🌈 데이터 소스 필수 메서드

  • numberOfItemsInSection : 섹션당 항목 수를 반환
  • 아래 예시에서는 데이터의 수량만큼의 섹션 수를 반환하고 있다
  • 뷰당 보여줄 섹션 수가 한정되어 있다면 int 값으로도 설정 가능(3, 5, 10 등)
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return data.count
}

 

  • cellForItemAt : 셀을 생성하고 구성
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)

    //셀 내부 설정

    return cell
}

 

 

2-2.  UICollectionViewDelegate

: 사용자 상호작용을 처리

UICollectionViewDelegate는 콜렉션뷰에서 사용자가 상호작용할 때 발생하는 이벤트나 동작을 처리하기 위한 프로토콜

이를 통해 셀 선택, 하이라이트, 스크롤 이벤트 등을 쉽게 제어한다

 

1.  셀 선택: 사용자가 특정 셀을 터치했을 때 호출되는 메서드

2.  셀의 표시 여부 관리: 셀이 표시되기 직전, 또는 표시된 후에 호출되는 메서드

3.  하이라이트 및 선택 상태 관리: 셀을 하이라이트하거나 선택 상태를 변경할 때 호출되는 메서드

4.  스크롤 이벤트 관리: 사용자가 스크롤할 때 호출되는 메서드

5.  레이아웃 조정: 셀의 크기 또는 위치를 커스터마이즈할 수 있는 메서드

 

 

 


3.  컬렉션 뷰의 셀

3-1.  셀 재사용 메서드

: 기존의 셀을 재사용하게 만들어준다

//기본셀
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "BasicCell")

//커스텀셀
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: "CustomCell")

 

→ 이부분이 많이 헷갈려서 조금 더 찾아보고 정리한 내용 ⭐️⭐️⭐️

1. register의 역할
셀을 미리 등록해 둔다
등록한 셀을 재사용하여 성능을 최적화한다

2. CustomCollectionViewCell.self
등록할 셀의 클래스명(Custom~~Cell)을 전달하는 부분

3. forCellWithReuseIdentifier: "CustomCell"
셀을 재사용하기 위한 식별자
사용자가 임의로 지정하는 문자열이며, 추후 dequeueReusableCell 메서드를 호출할 때 이 식별자를 통해 해당 셀을 가져온다

 

 

[ 예시 ]

collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "BasicCell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BasicCell", for: indexPath)

 

 

3-2.  셀에 데이터 추가

: indexPath.row로 label에 순서대로 배열의 데이터를 넣어준다

label.text = data[indexPath.row]

 

 

3-3.  셀 선택 처리(didSelectItemAt)

: 유저가 셀을 선택했을 때 호출되는 메서드다(프린트문으로 예제)

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let selectedItem = data[indexPath.row]
    print("\(selectedItem) 선택됨")
}

 

 

 

 


4.  UICollectionViewFlowLayout

UICollectionViewFlowLayout란 컬렉션뷰에서 셀을 수평 또는 수직으로 스크롤이 가능한 그리드 형태로 배치한다
각 셀의 크기와 간격을 설정하고 헤더나 푸터를 추가할 수 있다

 

 

4-1.  자주 사용되는 속성

속성 이름 설명
itemSize 각 셀의 크기 (가로, 세로) 기본값은 50 x 50.
minimumLineSpacing 셀 사이의 수직 간격
minimumInteritemSpacing 셀 사이의 수평 간격
scrollDirection 스크롤 방향 .vertical (기본값) 또는 .horizontal
sectionInset 섹션 내부의 여백(상, 좌, 하, 우)
headerReferenceSize 섹션 헤더의 크기
footerReferenceSize 섹션 푸터의 크기

 

 

4-2.  기본적인 사용 방법

import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

    var collectionView: UICollectionView!
    let data = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]

    override func viewDidLoad() {
        super.viewDidLoad()

        // 1. 레이아웃 객체 생성
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 100, height: 100) // 셀 크기
        layout.minimumLineSpacing = 20 // 수직 간격
        layout.minimumInteritemSpacing = 10 // 수평 간격
        layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) // 섹션 여백

        // 2. 컬렉션뷰 초기화
        collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.delegate = self
        collectionView.dataSource = self

        // 3. 셀 등록
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")

        // 4. 컬렉션뷰 추가
        self.view.addSubview(collectionView)
    }

    // MARK: - UICollectionViewDataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        cell.backgroundColor = .blue

        // 셀의 텍스트 추가
        let label = UILabel(frame: cell.bounds)
        label.text = data[indexPath.row]
        label.textAlignment = .center
        label.textColor = .white
        cell.contentView.addSubview(label)

        return cell
    }
}

 

1)  레이아웃 객체 생성

  • UICollectionViewFlowLayout 객체를 생성한다
  • itemSize : 셀 크기 설정
  • minimumLineSpacing : 수직 간격
  • minimumInteritemSpacing : 수평 간격
  • sectionInset : 섹션의 여백   *섹션: 셀을 그룹으로 묶는 단위
let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 100, height: 100) // 셀 크기
        layout.minimumLineSpacing = 20 // 수직 간격
        layout.minimumInteritemSpacing = 10 // 수평 간격
        layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) // 섹션 여백

 

 

2)  컬렉션뷰 초기화

  • 컬렉션뷰 초기화 시 1번에서 설정한 UICollectionViewFlowLayout 객체 전달
let layout = UICollectionViewFlowLayout()
    // ~~ 레이아웃 설정 ~~
collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)

 

 

3)  나머지 일반 컬렉션뷰와 동일

 


 

5.  Custom UICollectionViewLayout

위에서 알아본 <4. UICollectionViewFlowLayout> 에서 제공하지 않는 복잡한 레이아웃을 구현한다
컬렉션뷰에서 셀, 헤더, 푸터를 배치하는 규칙을 정의하는 클래스이다
예를 들어 물결치는 레이아웃, 계단식 배치 등 커스텀 레이아웃을 만들 수 있다

 

 

5-1.  기본 작동 원리

셀의 위치 계산 : 셀이 화면의 어디에 배치될지 결정해야 한다

화면 크기 계산 : 전체 콘텐츠의 크기를 정의한다

레이아웃 속성 제공 : 컬렉션뷰가 각 셀이 어떻게 보여야할지 알게해야 한다

 

 

5-2.  핵심 메서드

CollectionViewLayout을 상속받아 메서드를 구현한다

메서드명 역할
prepare() 셀의 위치, 크기 등의 정보를 미리 계산
layoutAttributesForElements(in:) 특정 영역에 표시될 셀의 위치와 크기를 반환
layoutAttributesForItem(at:) 특정 셀의 레이아웃 속성을 반환
collectionViewContentSize 컬렉션뷰의 전체 콘텐츠 크기를 반환 (스크롤 가능한 크기)

 

 

 

5-3.  기본적인 사용방법

import UIKit

class PinterestLayout: UICollectionViewLayout {
    private var cache: [UICollectionViewLayoutAttributes] = [] // 셀 위치를 저장하는 캐시
    private var contentHeight: CGFloat = 0 // 콘텐츠의 전체 높이
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }

    override var collectionViewContentSize: CGSize {
        // 전체 콘텐츠 크기를 반환 (스크롤 가능 영역 정의)
        return CGSize(width: contentWidth, height: contentHeight)
    }

    override func prepare() {
        // 레이아웃 준비: 셀의 위치를 계산
        guard let collectionView = collectionView else { return }

        let numberOfColumns = 2 // 2열 레이아웃
        let cellPadding: CGFloat = 8 // 셀과 셀 사이 여백
        let columnWidth = contentWidth / CGFloat(numberOfColumns) // 열 너비
        var xOffset: [CGFloat] = [] // 각 열의 X 좌표
        for column in 0..<numberOfColumns {
            xOffset.append(CGFloat(column) * columnWidth)
        }
        var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns) // 각 열의 Y 좌표
        var column = 0 // 현재 열

        for item in 0..<collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: item, section: 0)

            // 임의로 셀 높이를 설정 (예: 랜덤 높이)
            let cellHeight = CGFloat(arc4random_uniform(100) + 150)
            let height = cellHeight + cellPadding * 2
            let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)

            // 레이아웃 속성 생성
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes) // 속성 캐시에 저장
            contentHeight = max(contentHeight, frame.maxY) // 콘텐츠 높이 업데이트
            yOffset[column] = yOffset[column] + height // 현재 열의 Y 좌표 업데이트

            // 다음 열로 이동
            column = column < (numberOfColumns - 1) ? (column + 1) : 0
        }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // rect(화면에 보이는 영역) 안에 포함된 셀의 레이아웃 속성을 반환
        return cache.filter { $0.frame.intersects(rect) }
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // 특정 셀의 레이아웃 속성을 반환
        return cache[indexPath.item]
    }
}