[TIL]CompositionalLayout + DiffableDataSource

숑이·2023년 9월 14일
0

iOS

목록 보기
21/26
post-thumbnail

CompositionalLayout을 사용해서 CollectionView의 섹션마다 다른 레이아웃을 구성하고, DiffableDataSource를 통해 필요한 데이터 소스를 제공해서 CollectionView를 구현해보겠습니다.

코드에 대한 설명은 하지 않겠습니다.

아래 링크를 참고해주세요
CompositionalLayout
DiffableDataSource

결과물

3개의 섹션으로 나누고, 하단의 버튼을 클릭할 때마다 데이터를 업데이트하는 예제입니다.
DiffableDataSource를 통해 데이터 소스를 제공하기 때문에 현재 상태 Snapshot과 변경된 Snapshot을 비교해서 필요한 부분만 UI 업데이트(삽입/삭제/재정렬 등)를 수행합니다.
따라서 위와 같이 부드러운 애니메이션을 적용할 수 있습니다.

Section, Item 타입 정의


enum DiffableSection: Hashable, CaseIterable {
    case circle
    case slide
    case main
    // case etc... 섹션 추가
}

enum DiffableSectionItem: Hashable {
    case circleItem(CircleItemModel)
    case slideItem(SlideItemModel)
    case mainItem(MainItemModel)
    // case ectItem... 섹션 아이템 추가
    
    struct CircleItemModel: Hashable {
        let title: String
    }
    
    struct SlideItemModel: Hashable {
        let title: String
    }
    
    struct MainItemModel: Hashable {
        let title: String
    }
}
typealias MyDataSource = UICollectionViewDiffableDataSource<DiffableSection, DiffableSectionItem>
typealias MySnapshot = NSDiffableDataSourceSnapshot<DiffableSection, DiffableSectionItem>

CompositionalLayout

private lazy var collectionView: UICollectionView = {
        let cv = UICollectionView(frame: .zero, collectionViewLayout: self.makeFlowLayout())
        cv.register(RectangleCell.self, forCellWithReuseIdentifier: RectangleCell.identifier)
        cv.register(CircleCell.self, forCellWithReuseIdentifier: CircleCell.identifier)
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: circleSectionHeaderKind, withReuseIdentifier: circleSectionHeaderKind)
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: mainSectionHeaderKind, withReuseIdentifier: mainSectionHeaderKind)
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: slideSectionHeaderKind, withReuseIdentifier: slideSectionHeaderKind)
        return cv
    }()
    
    
    //MARK: - Make CollectionView Compositional Layout
extension Diffable_CompositionalViewController {
    
    private func makeFlowLayout() -> UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { section, ev -> NSCollectionLayoutSection? in
            // section에 따라 서로 다른 layout 구성
            switch DiffableSection.allCases[section] {
            case .circle:
                return self.makeCircleSectionLayout()
            case .slide:
                return self.makeSlideSectionLayout()
            case .main:
                return self.makeMainSectionLayout()
            }
        }
    }
    
    private func makeCircleSectionLayout() -> NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .absolute(80),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        // group
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .absolute(90),
            heightDimension: .estimated(80))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        // section
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)
        
        // header
        let header = makeHeaderView(elementKind: circleSectionHeaderKind)
        section.boundarySupplementaryItems = [header]
        
        return section
    }
    
    private func makeSlideSectionLayout() -> NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        // group
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(0.8),
            heightDimension: .fractionalHeight(0.2))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        group.contentInsets = NSDirectionalEdgeInsets(
            top: 0,
            leading: 0,
            bottom: 0,
            trailing: 10)
        
        // section
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)
        
        section.orthogonalScrollingBehavior = .groupPagingCentered
        
        // header
        let header = makeHeaderView(elementKind: slideSectionHeaderKind)
        section.boundarySupplementaryItems = [header]
        
        return section
    }
    
    private func makeMainSectionLayout() -> NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(
            top: 0,
            leading: 0,
            bottom: 10,
            trailing: 0)
        
        // group
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(0.5))
        
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        // section
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)
        // header
        let header = makeHeaderView(elementKind: mainSectionHeaderKind)
        section.boundarySupplementaryItems = [header]
        
        return section
    }
    
    private func makeHeaderView(elementKind: String) -> NSCollectionLayoutBoundarySupplementaryItem {
        let headerSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .estimated(50))
        let header = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerSize,
            elementKind: elementKind,
            alignment: .top)
        
        return header
    }
}

DiffableDataSource


//MARK: - DataSource
extension Diffable_CompositionalViewController {
    
    // Diffable DataSource 정의
    // 파라미터로 collectionView와 cellProvider 전달
    // cellProvider를 통해 UI에 표시할 셀을 리턴함
    private func setupDataSource() {
        dataSource = .init(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
            switch item {
            case .mainItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            case .slideItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            case .circleItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CircleCell.identifier, for: indexPath) as! CircleCell
                cell.bind(text: model.title)
                return cell
            }
        })
        
        dataSource?.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -> UICollectionReusableView? in
            // header, footer...
            switch elementKind {
            case self.circleSectionHeaderKind:
                let header = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: self.circleSectionHeaderKind, for: indexPath) as! SectionHeader
                header.bind(sectionTitle: "Circle Section")
                return header
            case self.mainSectionHeaderKind:
                let header = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: self.mainSectionHeaderKind, for: indexPath) as! SectionHeader
                header.bind(sectionTitle: "Main Section")
                return header
            case self.slideSectionHeaderKind:
                let header = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: self.slideSectionHeaderKind, for: indexPath) as! SectionHeader
                header.bind(sectionTitle: "Slide Section")
                return header
            default:
                return nil
            }
        }
    }
    
    // DiffableDataSource는 Snapshot을 사용해서 CollectionView 또는 TableView에 데이터를 제공
    // 스냅샷을 사용해서 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영함
    // 스냅샷의 데이터는 표시하려는 Section과 Item으로 구성됨.
    private func updateSnapshot(
        circleSectionItems: [DiffableSectionItem],
        slideSectionItems: [DiffableSectionItem],
        mainSectionItems: [DiffableSectionItem]
    ) {
        var snapshot = MySnapshot() // 스냅샷 생성
        snapshot.appendSections(DiffableSection.allCases) // Section 추가
        // toSection 파라미터에 Section을 전달해서 아이템을 전달할 섹션을 명시함.
        // toSection 파라미터를 사용하지 않으면, 자동으로 마지막 섹션으로 전달됨.
        snapshot.appendItems(circleSectionItems, toSection: .circle)
        snapshot.appendItems(mainSectionItems, toSection: .main)
        snapshot.appendItems(slideSectionItems, toSection: .slide)
        dataSource?.apply(snapshot, animatingDifferences: true) // 데이터 새 상태 반영
    }
    
}
profile
iOS앱 개발자가 될테야

0개의 댓글