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