-
Compositional Layout+ Diffable Datasource 활용 가이드개발/Swift 2024. 8. 27. 18:33
Section 영역 은 3가지 타입이며
enum 내부에서 각각 레이아웃을 들고 있도록 하여 viewcontroller의 복잡도를 낮추었다.
public enum FAQSection: Hashable { case tag case faq case bottomGuide public var layoutSize: NSCollectionLayoutSection { switch self { case .tag: let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(50), heightDimension: .absolute(36)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(36)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.interItemSpacing = .fixed(8) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 8 section.contentInsets = .init(top: 12, leading: 16, bottom: 12, trailing: 16) let background = NSCollectionLayoutDecorationItem.background(elementKind: FAQBackgroundDecorationView.id) section.decorationItems = [background] return section case .faq: let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(48))) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(48)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) group.interItemSpacing = .fixed(1) let section = NSCollectionLayoutSection(group: group) section.contentInsets = .init(top: 8, leading: 0, bottom: 8, trailing: 0) return section case .bottomGuide: let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(154))) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(154)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 1) let section = NSCollectionLayoutSection(group: group) return section } } }
- NSCollectionLayoutDecorationItem.background 를 통해 특정 섹션에만 다른 백그라운드 색상을 가지고있도록 함
- tag는 너비가 동적인 셀들이 flow하게 나열되도록 (디바이스 너비에서 아래 줄로 위치) 함
- faq는 리스트 타입이고 동적인 높이를 가짐
- bottomguide는 1개 셀만 존재
public enum FAQCellData: Hashable { case tag(tag: any FAQTagDisplayable, isSelected: Bool) case faq(item: any FAQItemDisplayable, isCollapsed: Bool) case bottomGuide var id: String { switch self { case .tag: return FAQTagCell.id case .faq: return FAQItemCell.id case .bottomGuide: return FAQBottomGuideCell.id } } public func hash(into hasher: inout Hasher) { switch self { case let .faq(item, _): hasher.combine(item) case let .tag(tag, _): hasher.combine(tag) default: return } } public static func == (lhs: FAQCellData, rhs: FAQCellData) -> Bool { switch (lhs, rhs) { case let (.tag(lhsTag, lhsSelected), .tag(rhsTag, rhsSelected)): return lhsTag.id == rhsTag.id && lhsSelected == rhsSelected case let (.faq(lhsItem, lhsSelected), .faq(rhsItem, rhsSelected)): return lhsItem.id == rhsItem.id && lhsSelected == rhsSelected default: return false } } } protocol FAQCellProtocol: AnyObject { func apply(cellData: FAQCellData) }
item 으로 Cell Data 를 사용 하여 Cell 을 그릴때 어떤 Cell사용할지 활용되도록 함
associatedValue Enum 타입으로 필요한 데이터 타입을 전달해줌
FAQTagDisplayable 같은 protocol 타입을 전달하기 떄문에 Hashable 메소드들을 정의해줌
== 같은 경우 정확히 비교를 해줘야 hashable의 장점 (속도) 를 가져갈수 있음
VM
public let snapshot: Observable<NSDiffableDataSourceSnapshot<FAQSection, FAQCellData>> snapshot = Observable.combineLatest(selectedTagId, tagList, selectedFaqList, expandedFAQSet) .map({ selectedTagId, tagList, faqList, expandedFAQSet in var snapshot = NSDiffableDataSourceSnapshot<FAQSection, FAQCellData>() snapshot.appendSections([.tag]) let tagCells = tagList.map { tag in FAQCellData.tag(tag: tag, isSelected: tag.id == selectedTagId) } snapshot.appendItems(tagCells, toSection: .tag) snapshot.appendSections([.faq]) let faqCells = faqList.map { faqItem in FAQCellData.faq(item: faqItem, isCollapsed: !expandedFAQSet.contains { $0 == faqItem.id }) } snapshot.appendItems(faqCells, toSection: .faq) snapshot.appendSections([.bottomGuide]) snapshot.appendItems([.bottomGuide], toSection: .bottomGuide) return snapshot })
snapshot 을 VC에서 옵져빙하여 사용할수 있도록 함
스냅샷 데이터 구성은 비교적 쉽게 구현 가능
VC
viewModel.snapshot .observe(on: MainScheduler.instance) .bind { [weak self] snapshot in let currentOffset = self?.collectionView.contentOffset self?.diffableDataSource?.apply(snapshot, animatingDifferences: false) if let currentOffset = currentOffset { self?.collectionView.setContentOffset(currentOffset, animated: false) } }.disposed(by: disposeBag)
snapshot 바인딩 하여서 데이터소스에 적용, 적용하면 컨텐츠 길이변화 때문에 스크롤위치가 이상하여 스크롤 위치 보존하는 로직 필요함
diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: item.id, for: indexPath) (cell as? FAQCellProtocol)?.apply(cellData: item) if let cell = cell as? FAQItemCell { cell.tapHandler = { [weak self] id in self?.viewModel.openOrCollapseFAQ(id: id) } } if let cell = cell as? FAQBottomGuideCell { cell.tapHandler = {[weak self] in AnalyticsHelper.shared.log(.touchFAQInquiryButton) self?.coordinator.presentQuestionVC() } } return cell })
CellProvider 적용 부분, apply 공통 적용 가능 하도록 모든 cell이 FAQCellProtocol 준수하도록 구현됨
item.id 는 아까 enum 에서 구현되었으므로 item 마다 필요한 cell 타입을 가져다 쓸수 있음
Section 과 Item 정의가 잘 되어있으면 비교적 짧고 간단하게
VC와 VM 구현을 할수 있었다.
'개발 > Swift' 카테고리의 다른 글
Label Attribute 이미지 처리, 줄넘김 하기 (0) 2024.09.12 Contact Framework 사용하여 연락처 불러오기 (0) 2024.09.03 [Error] suspend resume partial function for XXX (0) 2024.08.20 [Error] navigationbar subview 레이아웃 에러 (0) 2024.08.20 [Swift] CoreData 사용해보기: CRUD 구현 가이드 (0) 2024.05.18