ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 구현을 할수 있었다.

    댓글

Designed by Tistory.