UICollectionView Compositional Layout으로 CustomCell 쉽게 연동하기

RYEOL·2024년 8월 5일

Swift

목록 보기
11/15

UICollectionView Compositional Layout와 CustomCell 연동하기

같은 사진이나 스크롤뷰를 각각 다른 모양과 크기로 보여줘야 할 때, UICollectionView는 아주 유용한 도구입니다. 하지만, 아이템들이 다양하게 섞일 경우 UICollectionView를 알맞게 설정하려고 할 때 꽤 골치 아파질 수 있습니다. 이번 글에서는 UICollectionView를 효과적으로 구성하고 CustomCell과 연동하는 방법에 대해 자세히 설명합니다.

시작하기 전에, UICollectionView 기본 개념

여러분들이 이미 UICollectionView의 기본 설정에 익숙하다고 가정하고, 우리는 콤포지셔널 레이아웃과 커스텀 셀을 중심으로 이야기해보겠습니다.

첫번째 단계: 커스텀 셀 만들기

UICollectionView의 셀이 너무 단순하다면 독자가 이해하기 힘들 수 있습니다. 그래서 커스텀 셀을 만듭니다.

import UIKit
import SnapKit
import Then

class CustomCell: UICollectionViewCell {
    static let reuseIdentifier = "CustomCell"
    
    private let iconImageView = UIImageView().then {
        $0.contentMode = .scaleAspectFit
        $0.tintColor = .systemBlue
    }
    
    private let titleLabel = UILabel().then {
        $0.font = .systemFont(ofSize: 16, weight: .medium)
        $0.textAlignment = .center
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        contentView.do {
            $0.backgroundColor = .systemGray6
            $0.layer.cornerRadius = 16
            $0.addSubview(iconImageView)
            $0.addSubview(titleLabel)
        }
        
        iconImageView.snp.makeConstraints {
            $0.top.equalToSuperview().offset(20)
            $0.centerX.equalToSuperview()
            $0.width.height.equalTo(50)
        }
        
        titleLabel.snp.makeConstraints {
            $0.top.equalTo(iconImageView.snp.bottom).offset(10)
            $0.leading.trailing.equalToSuperview().inset(10)
            $0.bottom.equalToSuperview().inset(20)
        }
    }
    
    func configure(with title: String, iconName: String) {
        titleLabel.text = title
        iconImageView.image = UIImage(systemName: iconName)
    }
}

이번 코드 샘플에서는 SnapKit과 Then 라이브러리를 사용하여 UI 요소들을 손쉽게 추가하고 제약을 설정했습니다. setupViews() 메서드에서 contentView에 아이콘과 라벨을 추가하고 제약 조건을 설정해 매우 직관적으로 UI를 조립할 수 있습니다.

두 번째 단계: 데이터 모델 정의하기

이번에는 각 셀에 표시할 데이터를 어떻게 관리할 수 있는지 설명하겠습니다. 이를 위해 데이터 모델을 정의합니다.

struct MenuItem: Hashable {
    let title: String
    let iconName: String
    
    init(title: String, iconName: String) {
        self.title = title
        self.iconName = iconName
    }
}

Hashable 프로토콜을 채택하여 NSDiffableDataSourceSnapshot에 문제 없이 사용될 수 있도록 합니다.

세 번째 단계: UICollectionView 설정

UIViewController를 상속받은 클래스에서 UICollectionView를 설정합니다. UICollectionView는 데이터를 관리하고 사용자와 상호작용하는 중요한 역할을 합니다.

import UIKit

class CustomViewController: UIViewController {
    
    enum Section: Hashable {
        case navigation
        case carousel
        case menu
        case pharmacyMap
    }
    
    typealias DataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>
    typealias Snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>
    
    var collectionView: UICollectionView! = nil
    var dataSource: DataSource! = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureHierarchy()
        configureDataSource()
    }
    
    private func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        view.addSubview(collectionView)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
}

이 코드에서는 컬렉션 뷰를 설정하고 이를 뷰 hierarchy에 추가합니다. layout을 생성하는 로직은 createLayout() 메서드에서 구현합니다.

네 번째 단계: 데이터 소스를 설정하기

이제 데이터 소스를 설정합니다. 이는 컬렉션 뷰에 데이터를 공급하고 셀을 구성하는 역할을 합니다.

private func configureDataSource() {
    let menuCellRegistration = UICollectionView.CellRegistration<CustomCell, MenuItem> { cell, indexPath, item in
        cell.configure(with: item.title, iconName: item.iconName)
    }
    
    dataSource = DataSource(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in
        switch item {
        case let item as MenuItem:
            return collectionView.dequeueConfiguredReusableCell(using: menuCellRegistration, for: indexPath, item: item)
        default:
            return nil
        }
    }
}

private func performQuery() {
    var snapshot = Snapshot()
    snapshot.appendSections([.menu])
    let items = [
        MenuItem(title: "약 검색", iconName: "magnifyingglass"),
        MenuItem(title: "약 복용 시간 알림", iconName: "bell")
    ]
    snapshot.appendItems(items, toSection: .menu)
    dataSource.apply(snapshot, animatingDifferences: true)
}

DataSource는 컬렉션 뷰의 셀을 조립하고 데이터를 제공합니다. performQuery() 메서드에서는 섹션과 아이템을 스냅샷에 추가하여 디퓨저블 데이터 소스에게 넘겨줍니다. 이렇게 하면 컬렉션 뷰에 데이터를 손쉽게 반영할 수 있습니다.

마지막 단계: 셀 선택 시 동작 처리

셀을 선택했을 때 어떤 동작을 할지 설정합니다. 델리게이트 메서드인 collectionView(_:didSelectItemAt:)를 사용하면 됩니다.

extension CustomViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let section = dataSource.snapshot().sectionIdentifiers[indexPath.section]
        switch section {
        case .menu:
            if let item = dataSource.itemIdentifier(for: indexPath) as? MenuItem {
                switch item.title {
                case "약 검색":
                    print("약 검색 기능 선택됨")
                case "약 복용 시간 알림":
                    print("약 복용 시간 알림 기능 선택됨")
                default:
                    break
                }
            }
        default:
            break
        }
    }
}

이 델리게이트 메서드는 사용자가 어떤 셀을 선택했는지에 따라 다른 동작을 수행합니다. 각 메뉴 아이템에 대한 특정 동작을 정의하기 위해 스위치 문을 사용했습니다.

결론

이번 글에서는 엄청난 유연성을 제공하는 UICollectionView를 효과적으로 사용하기 위해 CustomCell을 만들고 데이터 소스를 설정하는 방법을 배웠습니다. 기본 단계들을 잘 숙지하면 여러분들도 컬렉션 뷰를 활용하여 다양한 UI를 손쉽게 구현할 수 있습니다.

profile
Flutter, Swift 모바일 개발자의 스타트업에서 살아남기

0개의 댓글