[iOS] DiffableDataSource n-Section n-Item (섹션마다 다른 셀)
DiffableDataSource을 이용하여 각각의 섹션마다 다른 셀을 적용하고자 함.
앱 타겟은 iOS 13
iOS 14이상의 경우 아주 간단하게 처리할 수 있는데,
UICollectionView.CellRegistration
이는 애플의 공식문서에도 나와있음 다만, iOS 14이상에서 가능
⚙️개발환경⚙️
Xcode 14.0
iOS 13.0 이상
Swift 5
SnapKit 5.6
🚨 코드를 보고난 후 주의할 점 🚨
1. 기본적으로 Item의 경우에는 Hashable해야 함.
결과 이미지의 세번째 사진을 보면 배열에 1이 4개가 들어있음에도 불구하고 1이 하나만 보여짐.
그 이유는 Hashable하기 때문에 Set으로 처리 되어서 같은 값이 경우에는 하나로 인식
따라서 model을 정의하여 사용할 때 uuid를 활용하여 Equtable를 상속받아 func == 을 잘 정의해주기.
만약 같은 Unique하지 않게 값이 들어가는 경우라면 콘솔에 아래와 같은 에러는 보여준다.
특히, 아래의 에러가 중복되는 수 만큼 찍혀서 디버그하기가 용이하다.
```
[UIDiffableDataSource] Diffable data source detected an attempt to insert or append 1 item identifier that already exists in the snapshot. The existing item identifier will be moved into place instead, but this operation will be more expensive. For best performance, inserted item identifiers should always be unique. Set a symbolic breakpoint on BUG_IN_CLIENT_OF_DIFFABLE_DATA_SOURCE__IDENTIFIER_ALREADY_EXISTS to catch this in the debugger. Item identifier that already exists: AppleCollectionView.MuseumViewController.Item.number
```
2. DataSource 후 snapshot 호출
순서를 지키지 않으면 앱이 크래시 난다.
✅ 결과 이미지 ✅
✅ 전체 코드 ✅
//
// MuseumViewController.swift
// AppleCollectionView
//
// Created by Hamlit Jason on 2022/09/18.
//
import UIKit
import SnapKit
final class MuseumViewController: UIViewController, UITableViewDelegate {
// 🐬 1. 섹션 정의
enum MySection: Int { // 1-1. Int를 상속받아야 section값 int로 찾을 수 있음
case banner
case number
}
// 🐬 2. 아이템 정의
enum Item: Hashable {
case bannerItem(String)
case numberItem(Int)
}
// 🐬 3. 테이블 뷰 및 DataSource 선언
var tableView = UITableView()
var dataSource: UITableViewDiffableDataSource<MySection, Item>!
var banners: [String] {
[
String.createRandomString(length: 100),
String.createRandomString(length: 30),
String.createRandomString(length: 50),
String.createRandomString(length: 25),
String.createRandomString(length: 50),
String.createRandomString(length: 5),
String.createRandomString(length: 25),
String.createRandomString(length: 35),
String.createRandomString(length: 45),
String.createRandomString(length: 5),
]
}
var numbers: [Int] = [100000, 10000, 1000, 100, 10, 10, 12, 13, 14, 15, 16, 71, 81, 91, 31, 41, 1, 1, 1, 1]
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
fetchDataSource()
fetchSnapShot()
}
func setupTableView() {
// 🐬 4. 테이블 뷰 설정
// 🐬 표시가 있는것이 diffable과 관련한 코드
// 4-0. 테이블 뷰 view에 추가
view.addSubview(tableView)
// 4-1. header 및 footer를 사용하기 위해서
tableView.delegate = self
// 🐬 4-2. 테이블 뷰에 사용되는 cell 연결
tableView.register(MuseumBannerCell.self, forCellReuseIdentifier: MuseumBannerCell.id)
tableView.register(MuseumNumberCell.self, forCellReuseIdentifier: MuseumNumberCell.id)
// 4-3. 테이블 뷰에 사용되는 헤더 view 연결
tableView.register(MuseumHeaderView.self, forHeaderFooterViewReuseIdentifier: MuseumHeaderView.id)
// 4-4. 레이아웃 및 배경색 지정
tableView.backgroundColor = .green
tableView.snp.makeConstraints {
$0.edges.equalTo(view.safeAreaLayoutGuide)
}
}
// 🐬 5. 스냅샷을 통한 데이터 연결 - ✅ 중요
func fetchSnapShot() {
// 🐬 5-1. 스냅샷 데이터 소스 만들기
var snapshot = NSDiffableDataSourceSnapshot<MySection, Item>()
// 🐬 5-2. 스냡샷에 섹션추가
snapshot.appendSections([.banner, .number])
// 🐬 5-3. 섹션를 구분하여 아이템 append
banners.map { snapshot.appendItems([.bannerItem($0)], toSection: .banner) }
numbers.map { snapshot.appendItems([.numberItem($0)], toSection: .number) }
// 🐬 5-4. 데이터 소스에 적용!
dataSource.apply(snapshot)
/* 🚨 주의사항!
섹션이 여러개일 경우 appendItems 시 toSection: 을 지정하지 않으면, 마지막 섹션에 데이터가 몰아서 처리된다.
*/
}
// 🐬 6. 데이터 소스 정의하기 - ✅ 매우 중요
func fetchDataSource() {
dataSource = UITableViewDiffableDataSource<MySection, Item>(
tableView: tableView
) { tableView, indexPath, itemIdentifier in
// 🐬 6-1. section 정의하기
let section = MySection(rawValue: indexPath.section)
print("section: \(section): indexPath \(indexPath): item \(itemIdentifier) \(type(of: itemIdentifier))")
// 🐬 6-2. itemIdentifier를 가지고 아이템 값 추출하기
var value: Any
switch itemIdentifier {
case .bannerItem(let item):
value = item
case .numberItem(let item):
value = item
}
// 🐬 6-3. 섹션에 따른 값 설정하기
switch section {
case .banner:
guard let cell = tableView.dequeueReusableCell(
withIdentifier: MuseumBannerCell.id,
for: indexPath
) as? MuseumBannerCell else { return UITableViewCell() }
print("BANNER")
cell.configureCell(with: value as! String)
cell.backgroundColor = .red
return cell
case .number:
guard let cell = tableView.dequeueReusableCell(
withIdentifier: MuseumNumberCell.id,
for: indexPath
) as? MuseumNumberCell else { return UITableViewCell() }
print("NUMBER")
cell.configureCell(with: value as! Int)
cell.backgroundColor = .blue
return cell
default:
return UITableViewCell()
}
}
}
// MARK: - Header
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
return MuseumHeaderView()
} else {
return MuseumHeaderView()
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
return 100
} else {
return 150
}
}
// MARK: FOOTER
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0
}
}
class MuseumBannerCell: UITableViewCell {
static let id: String = "MuseumBannerCell"
let title = UILabel()
// MARK: - Initialize
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(title)
title.numberOfLines = 0
title.snp.makeConstraints {
$0.edges.equalToSuperview().inset(20)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(with item: String) {
title.text = item
}
}
final class MuseumNumberCell: UITableViewCell {
static let id: String = "MuseumNumberCell"
let title = UILabel()
// MARK: - Initialize
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(title)
title.snp.makeConstraints {
$0.edges.equalToSuperview().inset(20)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(with item: Int) {
title.text = "\(item)"
}
}
class MuseumHeaderView: UITableViewHeaderFooterView {
static let id: String = "MuseumBannerHeaderView"
let headerTitle = UILabel()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
addSubview(headerTitle)
headerTitle.snp.makeConstraints {
$0.edges.equalToSuperview()
}
headerTitle.text = "헤더"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
String+
extension String {
static func createRandomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String(
(0..<length)
.map { _ in letters.randomElement()! }
)
}
}
'apple > iOS' 카테고리의 다른 글
[Moya] Unable to parse empty data 대응하기 (0) | 2022.09.29 |
---|---|
[iOS] UICollectionView CompositionalLayout (0) | 2022.09.18 |
Realm 간단하게 구조 적용하기 (0) | 2022.09.06 |
[Realm] Realm migration (Swift) (0) | 2022.09.05 |
[Realm] The document “default.realm” could not be opened. (0) | 2022.09.05 |