iOS 14이상부터 UICollectionView를 구현하기 위해서 다양한 API를 제공하는데 해당 API들을 사용하면 장점들을 활용할 수 있습니다.
iOS 14이전방식으로 예시화면을 UICollectionView구현한 코드를 살펴보고 새로운 API들을 하나씩 적용하여 변경하면서 살펴보겠습니다.
👨🏻🏫 예제코드 설명
- 해당 예제코드를 살펴보기전에 레이아웃은 SnapKit 라이브러리를 활용하였고, Then 라이브러리를 활용하여 UIComponent을 구성하였습니다.
- 파일명은 iOS14이전으로 구현한 코드들은 앞에 Previous를 붙여주고 iOS 14이상방식으로 구현한 코드들은 New로 명명합니다.
- 아래 이미지를 참고하여 UICollectionView를 구현할 예정입니다.
🆓 iOS 14 이전방식으로 UICollectionView구현
1. CustomView 생성 및 등록
이미지를 참고하면 Section별로 Header가 존재하고 각각의 CustomCollectionViewCell이 존재하기 때문에 해당 CustomView들을 생성합니다.
- CollectionView에서 Section별 HeaderView를 구현하기 위해서는 UICollectionReusableView를 상속받는 CustomView를 생성해야합니다.
- CollectionView에서 CustomCell은 UICollectionViewCell을 상속받습니다.
PreviousCollectionViewCell.swift
import UIKit
import SnapKit
import Then
final class PreviousCollectionViewCell: UICollectionViewCell {
static let reuseID = String(describing: PreviousCollectionViewCell.self)
lazy var textLabel = UILabel().then {
$0.font = .systemFont(ofSize: 14, weight: .bold)
$0.textColor = .systemBrown
}
override init(frame: CGRect) {
super.init(frame: frame)
setupHierarchy()
setupLayout()
setupProperties()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupHierarchy() {
self.contentView.addSubview(self.textLabel)
}
private func setupLayout() {
self.textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
private func setupProperties() {
self.backgroundColor = .systemGray5
}
}
PreviousCollectionReusableView.swift
import UIKit
import SnapKit
import Then
final class PreviousCollectionReusableView: UICollectionViewCell {
static let reuseID = String(describing: PreviousCollectionReusableView.self)
lazy var textLabel = UILabel().then {
$0.font = .systemFont(ofSize: 14, weight: .bold)
$0.textColor = .systemBlue
}
override init(frame: CGRect) {
super.init(frame: frame)
setupHierarchy()
setupLayout()
setupProperties()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupHierarchy() {
self.contentView.addSubview(self.textLabel)
}
private func setupLayout() {
self.textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
}
private func setupProperties() {
self.backgroundColor = .systemGray3
}
}
2가지 CustomView 모두 텍스트를 가지는 UILabel과 배경색만 지정해주었습니다.
생성을 완료하면 UICollectionView의 register메서드를 이용해 각각의 CustomView를 등록합니다.
PreviousCollectionViewController.swift
/// iOS 14 이전 방식 기본적인 구현방법 사용한 CollectionView 예제코드
final class PreviousCollectionViewController: UIViewController {
lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()).then {
$0.dataSource = self
$0.register(PreviousCollectionViewCell.self, forCellWithReuseIdentifier: PreviousCollectionViewCell.reuseID)
$0.register(PreviousCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: PreviousCollectionReusableView.reuseID)
}
...
2. UICollectionView의 Layout 설정
UICollectionView 내부의 Section Header와 Item들의 사이즈, 간격등을 정의하기 위해 Layout을 설정합니다.
PreviousCollectionViewController.swift
/// iOS 14 이전 방식 기본적인 구현방법 사용한 CollectionView 예제코드
final class PreviousCollectionViewController: UIViewController {
lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()).then {
$0.dataSource = self
$0.register(PreviousCollectionViewCell.self, forCellWithReuseIdentifier: PreviousCollectionViewCell.reuseID)
$0.register(PreviousCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: PreviousCollectionReusableView.reuseID)
}
...
private func createLayout() -> UICollectionViewFlowLayout {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: 60, height: 60)
flowLayout.minimumInteritemSpacing = 4
flowLayout.minimumLineSpacing = 30
flowLayout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: 100)
return flowLayout
}
...
3. Item에 들어갈 Model 작성
텍스트로 제공할 값은 이름으로만 구성되어있고 static변수를 사용하여 미리 데이터를 정의합니다.
import Foundation
struct PreviousSampleModel {
var name: String
}
extension PreviousSampleModel {
static var allDatas = [
PreviousSampleModel(name: "김민준"),
PreviousSampleModel(name: "이서연"),
PreviousSampleModel(name: "박지호"),
PreviousSampleModel(name: "최하은"),
PreviousSampleModel(name: "정다현"),
PreviousSampleModel(name: "윤도윤"),
PreviousSampleModel(name: "장서아"),
PreviousSampleModel(name: "한지우"),
PreviousSampleModel(name: "송하린"),
PreviousSampleModel(name: "권민재")
]
}
4. UICollectionDataSource 연결 및 UICollectionView 지정
UICollectionView를 UIViewController의 SuperView에 추가한 후 레이아웃을 정의합니다.
UICollectionViewDataSource에 표시할 셀과 섹션 수를 지정합니다. (numberOfItemsInSection, numberOfSections)
셀의 구성과 헤더뷰의 구성을 정의합니다. (cellForItemAt, viewForSupplementaryElementOfKind)
PreviousCollectionViewController.swift
/// iOS 14 이전 방식 기본적인 구현방법 사용한 CollectionView 예제코드
final class PreviousCollectionViewController: UIViewController {
lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()).then {
$0.dataSource = self
$0.register(PreviousCollectionViewCell.self, forCellWithReuseIdentifier: PreviousCollectionViewCell.reuseID)
$0.register(PreviousCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: PreviousCollectionReusableView.reuseID)
}
private var datas: [PreviousSampleModel] = PreviousSampleModel.allDatas
override func viewDidLoad() {
super.viewDidLoad()
setupHierarchy()
setupLayout()
}
private func setupHierarchy() {
self.view.addSubview(self.collectionView)
}
private func setupLayout() {
self.collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func createLayout() -> UICollectionViewFlowLayout {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: 60, height: 60)
flowLayout.minimumInteritemSpacing = 4
flowLayout.minimumLineSpacing = 30
flowLayout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: 100)
return flowLayout
}
}
// MARK: - UICollectionViewDataSource
extension PreviousCollectionViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return datas.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PreviousCollectionViewCell.reuseID, for: indexPath) as! PreviousCollectionViewCell
let item = datas[indexPath.item]
cell.textLabel.text = "\(item.name)"
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: PreviousCollectionReusableView.reuseID, for: indexPath) as! PreviousCollectionReusableView
headerView.textLabel.text = "\(indexPath.section)번째 Section"
return headerView
}
return UICollectionReusableView()
}
}
🆕 iOS 14 이후방식으로 변경
1. UICompositional Layout으로 Layout 작성
iOS13에서 UICollectionViewCompositionalLayout이라는 레이아웃 객체가 추가되었습니다.
기존의 UICollectionViewFlowLayout에서 구현할 수 없는 다양한 레이아웃 설정이 가능하게 되었습니다.
가장 큰 특징은 하나 이상의 Section으로 구성되고 각각의 Item을 담고 있는 Group으로 구성되어 있습니다.
NewCollectionViewController.swift → PreviousCollectionViewController.swift의 createLayout메서드 변경
private func createLayout() -> UICollectionViewLayout {
let compositionalLayout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let columnCount = CGFloat(6)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1 / columnCount), heightDimension: .absolute(60))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 0)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(60))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(4)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
let headerView = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [headerView]
section.interGroupSpacing = 30
return section
}
return compositionalLayout
}
- 기존의 UICollectionFlowLayout생성하는 부분을 UICollectionCompositionalLayout으로 변경합니다. 화면 기준으로 6분할 하기 때문에 각각의 item을 전체너비의 6분할을 하였습니다.
- 기본적으로 item과 group모두 NSCollectionLayoutSize을 이용해서 사이즈를 설정합니다. 사이즈를 설정하는 옵션은 3가지가 있습니다.
- absolute: 고정 크기
- fractional: 현재 속한 컨테이너에서 차지하는 비율
- estimated: 크기가 변경될 가능성이 있는 경우 예상 크기
- group을 생성할 때 item을 어떤 방향으로 배치하는 group인지 생성할 때 정의합니다.
- horizontal : 수평 그룹
- vertical : 수직 그룹
- custom : 커스텀 그룹
- 기존의 headerView는 NSCollectionLayoutBoundarySupplementaryItem 객체를 생성하여 section의 boundarySupplementaryItems배열에 추가합니다.
2. UICollectionViewDataSource를 UICollectionViewDiffableDataSource로 변경
iOS 13 이전, UICollectionViewDataSource프로토콜을 채택하여 컬렉션뷰에 표시할 셀, 표시할 셀 수, 셀을 표시할 섹션등을 알려주었습니다.
iOS 13 이후, UICollectionViewDiffableDataSource를 도입되었는데 해당 객체는 이전에 표시된 데이터와의 차이를 자동으로 계산해줍니다.
UICollectionViewDiffableDataSource를 사용했을 때 장점 3가지
- 데이터를 추가, 업데이트, 삭제할때마다 자동으로 데이터가 변경되는 애니메이션을 얻을 수 있습니다.
- 컬렉션뷰와 데이터소스간의 데이터변경사항을 수동으로 관리하고 동기화하지 않아도 됩니다. (기본 데이터소스를 사용하여 동기화작업 중 하나라도 정렬이 잘못되면 오류가 발생합니다.)
- 전반적으로 코드가 감소하여 가독성이 올라갑니다.
구조를 살펴보면 제네릭클래스이고 Section과 Item으로 두가지 타입이 있는데 각각의 타입은 Hashable, Sendable을 준수하는 타입이여야 합니다.
class NewSampleModel: Hashable {
var id = UUID()
var name: String
init(name: String) {
self.name = name
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: NewSampleModel, rhs: NewSampleModel) -> Bool {
lhs.id == rhs.id
}
}
extension NewSampleModel {
static var allDatas = [
NewSampleModel(name: "김민준"),
NewSampleModel(name: "이서연"),
NewSampleModel(name: "박지호"),
NewSampleModel(name: "최하은"),
NewSampleModel(name: "정다현"),
NewSampleModel(name: "윤도윤"),
NewSampleModel(name: "장서아"),
NewSampleModel(name: "한지우"),
NewSampleModel(name: "송하린"),
NewSampleModel(name: "권민재")
]
static var subDatas = [
NewSampleModel(name: "김민준"),
NewSampleModel(name: "이서연"),
NewSampleModel(name: "박지호"),
NewSampleModel(name: "최하은"),
NewSampleModel(name: "정다현"),
NewSampleModel(name: "윤도윤"),
NewSampleModel(name: "장서아"),
NewSampleModel(name: "한지우"),
NewSampleModel(name: "송하린"),
NewSampleModel(name: "권민재")
]
}
- DiffableDataSource의 Item은 Hasable을 준수해야 하기 때문에 해당프로토콜을 준수합니다.
NewCollectionViewController.swift
final class NewCollectionViewController: UIViewController {
...
enum Section {
case first
case second
}
typealias DataSource = UICollectionViewDiffableDataSource<Section, NewSampleModel>
typealias SnapShot = NSDiffableDataSourceSnapshot<Section, NewSampleModel>
private var dataSource: DataSource!
private func setupProperties() {
self.dataSource = makeDatasource()
self.applySnapshot(animated: true)
}
private func makeDatasource() -> DataSource {
let dataSource = DataSource(collectionView: self.collectionView) { collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewCollectionViewCell.reuseID, for: indexPath) as! NewCollectionViewCell
cell.textLabel.text = "\(item.name)"
return cell
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
if kind == UICollectionView.elementKindSectionHeader {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: NewCollectionReusableView.reuseID,
for: indexPath) as? NewCollectionReusableView
view?.textLabel.text = "\(indexPath.section)번째 Section"
return view
}
return UICollectionReusableView()
}
return dataSource
}
private func applySnapshot(animated: Bool) {
var snapShot = SnapShot()
snapShot.appendSections([.first, .second])
snapShot.appendItems(NewSampleModel.allDatas, toSection: .first)
snapShot.appendItems(NewSampleModel.subDatas, toSection: .second)
dataSource.apply(snapShot, animatingDifferences: animated)
}
...
- DiffableDataSource의 Section으로 enum을 추가합니다. enum은 기본적으로 Hashable을 준수합니다.makeDatasource메서드를 작성하여 DataSource를 생성한 후 cell을 구성합니다.applySnapshot메서드를 사용하여 표시할 섹션 수와 아이템수를 제공하기 위해 Datasource가 참조하는 섹션과 아이템들을 저장합니다.
- 모두 완료되면 기존의 UICollectionViewDataSource 프로토콜을 제거합니다.
- 생성한 DataSource에 supplementaryViewProvider프로퍼티를 통해 커스텀헤더뷰를 구성합니다.
- 타입별칭을 활용하면 타입을 간략하게 축약하여 사용할 수 있습니다.
3. CellRegistration, SupplementaryRegistration 활용하여 미리 구성
iOS14부터 UICollectionView.CellRegistration 와 UICollectionView.SupplementaryRegistration 을 사용하면 미리 UICollectionView에 등록하지 않아도 되고 미리 구성할 수 있는 장점이 있습니다.
NewCollectionViewController.swift
private func makeDatasource() -> DataSource {
let cellRegistration = UICollectionView.CellRegistration<NewCollectionViewCell, NewSampleModel> { (cell, indexPath, item) in
cell.textLabel.text = "\(item.name)"
}
let dataSource = DataSource(collectionView: self.collectionView) { collectionView, indexPath, item in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
let headerRegistration = UICollectionView.SupplementaryRegistration<NewCollectionReusableView>(elementKind: UICollectionView.elementKindSectionHeader) { (header, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader {
header.textLabel.text = "\(indexPath.section)번째 Section"
}
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
return dataSource
}
- 기존의 CustomView들의 reuseID와 collectionView의 then클로저의 register를 제거할 수 있습니다.
4. UIContentConfiguration을 활용
UITableViewCell, UICollectionViewCell을 사용할 때 커스텀셀을 사용하지 않고 기본Cell을 사용할 때, iOS14이전에는 간단하게 기본Cell의 textLabel, detailTextLabel, imageView를 변경하여 설정할 수 있었지만 iOS14이후에는 defaultContentConfiguration을 통해 설정하도록 변경되었습니다.
간단한 UICollectionViewCell 또는 UICollectionViewReusableView를 사용할 때 UICollectionViewListCell을 이용할 수 있고 UIContentConfiguration을 통해 구성할 수 있습니다.
NewCollectionViewController.swift
...
private func makeDatasource() -> DataSource {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, NewSampleModel> { (cell, indexPath, item) in
var configuration = cell.defaultContentConfiguration()
configuration.text = "\(item.name)"
configuration.textProperties.font = .systemFont(ofSize: 14, weight: .bold)
configuration.textProperties.color = UIColor.systemBrown
var backgroundConfiguration = cell.defaultBackgroundConfiguration()
backgroundConfiguration.backgroundColor = .systemGray5
cell.contentConfiguration = configuration
cell.backgroundConfiguration = backgroundConfiguration
}
let dataSource = DataSource(collectionView: self.collectionView) { collectionView, indexPath, item in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (header, elementKind, indexPath) in
if elementKind == UICollectionView.elementKindSectionHeader {
var configuration = header.defaultContentConfiguration()
configuration.text = "\(indexPath.section)번째 Section"
configuration.textProperties.font = .systemFont(ofSize: 14, weight: .bold)
configuration.textProperties.color = UIColor.systemBlue
configuration.textProperties.alignment = .center
var backgroundConfiguration = header.defaultBackgroundConfiguration()
backgroundConfiguration.backgroundColor = .systemGray3
header.contentConfiguration = configuration
header.backgroundConfiguration = backgroundConfiguration
}
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
return dataSource
}
- 기존의 CustomCell, CustomReusableView를 UICollectionViewListCell로 변경합니다. 기존 커스텀뷰 파일들은 제거합니다.
- defaultConfiguration을 통해 UIListConfiguration을 통해 각각의 커스텀뷰를 구성합니다. 그 후 다시 ContentConfiguration과 BackgroundConfiguration에 구성들을 저장합니다.
'iOS > iOS (기본)' 카테고리의 다른 글
[iOS] Preview (실시간 미리보기) (43) | 2024.04.01 |
---|