- 애플 앱스토어의
Today
탭과 앱 상세정보 화면을 클론코딩한 앱
- MVVM, Clean Architecture, Unit Test, CompositionalLayout, SwiftLint
앱 상세화면은 다음과 같이 여러 종류의 section과 item으로 구성되어 있습니다.
![Screenshot 2023-07-17 at 14 16 03](https://private-user-images.githubusercontent.com/98260324/256777785-51e105b0-944d-4e39-8c72-d337e79c3356.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjUzMDY2ODcsIm5iZiI6MTcyNTMwNjM4NywicGF0aCI6Ii85ODI2MDMyNC8yNTY3Nzc3ODUtNTFlMTA1YjAtOTQ0ZC00ZTM5LThjNzItZDMzN2U3OWMzMzU2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MDIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTAyVDE5NDYyN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE5YWMzMjRjOWVkNGVmZTdmNTBjZjVkZjE1ZWYxM2U2ZjMyODNlMzAwZjM0ODI5NTQzODE5NzY4ZGVhMGJhNWQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.4dZYmKaFK2rFiQyRbbVEP_fIg5SIIRYTVG51aHmnLmY)
각 section별로 item의 타입이 다르기 때문에(TopItem
, SummaryItem
, ScreenshotItem
) DiffableDatasource
의 ItemIdentifier
타입으로 Hashable한 특정 구체 타입(예: TopItem
)을 지정할 수가 없습니다. 이 문제를 해결하기 위해 고려한 방법은 다음의 네 가지입니다.
ItemIdentifer
에 들어갈 타입을 열거형으로 정의 ⭐️ 선택한 방법ItemIdentifier
에 들어갈 타입을AnyHashable
로 정의ItemIdentifier
에 들어갈 타입으로 커스텀AnyHashable
타입을 구현DiffableDatasource
를 사용하지 않기
1~4번의 구현 방법 및 고려한 내용은 다음과 같습니다.
열거형과 연관값을 활용해 ItemIdentifier
의 타입을 정의할 수 있습니다.
- 장점:
DataSource
에 들어갈 객체의 타입을 열거형case
와연관값
을 활용해서 통제할 수 있다. - 단점: 열거형이기 때문에 case의 추가/삭제 시 해당 열거형과 관련된 모든
switch
문을 수정해야 할 수 있다.
// DetailItem.swift
enum DetailItem: Hashable {
case top(TopItem)
case summary(SummaryItem)
...
}
struct TopItem {
let appName: String
let appIconURL: String
...
}
struct SummaryItem {
let screenshotURL: String
...
}
// DetailViewController.swift
final class DetailViewController: UIViewController {
let dataSource = UICollectionViewDiffableDataSource<DetailSection, DetailItem>()
...
func configureDatasource() {
let topCellRegistration = UICollectionView.CellRegistration<DetailTopCell, DetailItem> { cell, _, item in
switch item {
case .top(let topItem):
cell.bind(with: topItem)
default:
return
}
}
...
}
...
}
- 장점: 구현 코드가 비교적 단순하다.
- 단점:
Hashable
한 객체라면 무엇이든DataSource
에 삽입할 수 있다.
// DetailItem.swift
struct TopItem: Hashable {
let appName: String
let appIconURL: String
...
}
// DetailViewController.swift
final class DetailViewController: UIViewController {
let dataSource = UICollectionViewDiffableDataSource<DetailSection, AnyHashable>()
...
func configureDatasource() {
let topCellRegistration = UICollectionView.CellRegistration<DetailTopCell, AnyHashable> { cell, _, item in
if let item = item as? TopItem {
cell.bind(with: item)
}
}
...
}
...
}
- 장점:
AnyHashable
보다DataSource
에 들어갈 수 있는 객체의 타입을 더 좁은 범위로 한정지을 수 있다.- 아래 예시의 경우
DetailItemType
을 채택한 객체로 타입을 한정할 수 있다.
- 아래 예시의 경우
- 단점: 구현이 필요 이상으로 복잡해질 수 있다.
// DetailItem.swift
protocol DetailItemType: Hashable { }
struct DetailBasicItem: Hashable {
let base: any DetailItemType
init(base: any DetailItemType) {
self.base = base
}
func hash(into hasher: inout Hasher) {
hasher.combine(base)
}
static func == (lhs: DetailBasicItem, rhs: DetailBasicItem) -> Bool {
if let lhsItem = lhs.base as? TopItem,
let rhsItem = rhs.base as? TopItem {
return lhsItem == rhsItem
} else if let lhsItem = lhs.base as? SummaryItem,
let rhsItem = rhs.base as? SummaryItem {
return lhsItem == rhsItem
} else {
return false
}
}
}
struct TopItem: DetailItemType { }
struct SummaryItem: DetailItemType { }
// DetailViewController.swift
final class DetailViewController: UIViewController {
let dataSource = UICollectionViewDiffableDataSource<DetailSection, DetailBasicItem>()
...
func configureDatasource() {
let topCellRegistration = UICollectionView.CellRegistration<DetailTopCell, DetailBasicItem> { cell, _, item in
let topItem = item.base as? TopItem
cell.bind(with: topItem)
}
...
}
...
}
DiffableDatasource
를 사용하지 않을 경우 indexPath
기반의 접근 방식을 사용해야 합니다.
- 장점: 비교적 구현이 단순하다.
- 단점: item의 삽입/삭제가 일어날 때 고려해야 할 점이 많아 휴먼 에러가 발생하기 쉽다.
위 화면에서는 앱 정보를 단순히 보여주기만 하기 때문에 item의 삽입/삭제가 일어나지 않아 4번을 채택하면 쉽게 해결할 수 있습니다. 하지만 DiffableDataSource
를 활용해 해당 문제를 해결하는 경험을 쌓고자 4번 방법은 배제하였습니다.
➡️ 최종적으로는 1번 ItemIdentifer
에 들어갈 타입을 열거형으로 정의를 선택했습니다. 그 이유는 DataSource
에 들어갈 객체의 타입을 가장 strict하게 통제할 수 있기 때문입니다.