iOS UIResponder UIView UICollectionView UICollectionViewDiffableDatasource

UICollectionViewDiffableDatasource

  • TableView(또는 CollectionView)를 그리기 위한 데이터를 관리하고 UI를 업데이트 하는 역할
  • Data Source와 달리 데이터가 달라진 부분을 추적하여 자연스럽게 UI를 업데이트
  • UI Data를 관리하고, 변경된 데이터만 UI를 자연스럽게 업데이트 해주는 객체
  • Hash 값을 이용하여 변화된 부분을 인지
  • Diffable Data Source를 이용하면 코드량이 줄어든다.
  • UI Transition
  • 데이터 자동 동기화로 에러를 방지

Image

특징

  • 소개     * WWDC19 Diffable Datasource
  • iOS 13부터 사용가능
  • 종류     * UITableViewDiffableDataSource     * UICollectionViewDiffableDataSource

Image
Image

UICollectionViewDiffableDataSource

Image

SectionIdentifierType

enum Section: CaseIterable {    
    case main 
}

UICollectionViewDiffableDataSource<Section, String>

ItenIdentifierType

class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject 
  where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> 
  where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
  • Diffable Data Source는 SectionIdentifierType, ItenIdentifierType 두개의 generic parameter로 결정된다.
  • 이 두 파라미터는 반드시 Hashable

apply시에 각 hash value를 비교하여 추가 or 삭제된 부분을 인지하게 된다.

Item Model

class Video: Hashable {
  var id = UUID()
  var title: String
  var thumbnail: UIImage?
  var lessonCount: Int
  var link: URL?
  
  init(title: String, thumbnail: UIImage? = nil, lessonCount: Int, link: URL?) {
    self.title = title
    self.thumbnail = thumbnail
    self.lessonCount = lessonCount
    self.link = link
  }
  
  static func == (lhs: Video, rhs: Video) -> Bool {
    lhs.id == rhs.id
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}

Section Model

class Section: Hashable {
  var id = UUID()
  var title: String
  var videos: [Video]
  
  init(title: String, videos: [Video]) {
    self.title = title
    self.videos = videos
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
  
  static func == (lhs: Section, rhs: Section) -> Bool {
    lhs.id == rhs.id
  }
}

CollectionView 적용

typealias DataSource = UICollectionViewDiffableDataSource<Section, Video>
func makeDataSource() -> DataSource {
  let dataSource = DataSource(
    collectionView: collectionView) { (collectionView, indexPath, video) -> UICollectionViewCell? in
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VideoCollectionViewCell", for: indexPath) as? VideoCollectionViewCell
      cell?.video = video
      return cell
    }
    
  dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
    guard kind == UICollectionView.elementKindSectionHeader else {
      return nil
    }
    
    let view = collectionView.dequeueReusableSupplementaryView(
      ofKind: kind,
      withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier,
      for: indexPath) as? SectionHeaderReusableView

    let section = self.dataSource.snapshot()
      .sectionIdentifiers[indexPath.section]
    view?.titleLabel.text = section.title
    return view
  }
  return dataSource
}

snapshot 생성 및 적용

typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Video>
func applySnapshot(animatingDifferrences: Bool = true) {
  var snapshot = Snapshot()
  snapshot.appendSections(sections)
  sections.forEach { section in
    snapshot.appendItems(section.videos, toSection: section)
  }
  dataSource.apply(snapshot, animatingDifferences: animatingDifferrences)
}

UITableViewDataSource, UICollectionViewDataSource와 차이점

  • UITableViewDataSource, UICollectionViewDataSource는 Protocol
    • UIViewController가 주로 제어
    • seciton의 개수 (the number of sections)
    • 각 section의 아이템의 개수 (the number of items)
    • cellForItemAt - cell의 render (content renders)
  • UITableViewDiffableDataSource Generic Class
    • UIViewController로 부터 분리 설계 가능
    • 추가적인 코드작업 없이도, 퀄리티 있는 에니메이션 적용 가능
    • 개선된 Data Source 매커니즘은 완벽하게 동기적인 버그나, 예외, 충돌 상황 제거
    • UI 데이터와 비지니스 로직 분리의 일관성
    • Identifier와 Snapshot을 사용해 간소화 된 Data 모델을 정의해, UI 랜더링

사용했을때 장점

Index 관련 오류 미 발생

image

DiffableDataSource를 사용 필요성

자연스럽게 UI를 업데이트 한다.

  • tableView.reloadData()
    • 해당 에러는 데이터의 변경 상황을 수동으로 관리하고 동기화 해야함
  • 사용자 경험(UX)
    • reloadData()의 경우 TableView를 모두 랜더링
    • Diffable Data Source를 사용하는 경우 변경된 데이터만 랜더링

성능

  • 속도 개선
    • O(N) (N: the number of item)
    • 모든 아이템의 hash 값을 비교해 보아야 하므로 선형 탐색 시간과 동일
  • BackgroundQueue
    • DataSource의 apply작업이 오래걸린다면, background queue에서 수행 가능
    • BackgroundQueue에서 이뤄지더라도 안전성 보장
    • Framework는 diffable을 계산이 완료되면 MainQueue 에서 결과 적용

UICollectionViewCompositionalLayout 적용

private func configureLayout() {
  collectionView.register(
    SectionHeaderReusableView.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier
  )
  
  collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
    let isPhone = layoutEnvironment.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiom.phone
    let size = NSCollectionLayoutSize(
      widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
      heightDimension: NSCollectionLayoutDimension.absolute(isPhone ? 280 : 250)
    )
    let itemCount = isPhone ? 1 : 3
    let item = NSCollectionLayoutItem(layoutSize: size)
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: itemCount)
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
    section.interGroupSpacing = 10
    
    // Supplementary header view setup
    let headerFooterSize = NSCollectionLayoutSize(
      widthDimension: .fractionalWidth(1.0),
      heightDimension: .estimated(20)
    )
    let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
      layoutSize: headerFooterSize,
      elementKind: UICollectionView.elementKindSectionHeader,
      alignment: .top
    )
    section.boundarySupplementaryItems = [sectionHeader]
    
    return section
  })
}

UICollectionViewCompositionalLayout에 대한 Post는 여기 가기로 가시면 되요~!😍

해당 프로젝트 보러 가기

출처

ZeddiOS
ellyheetov.devlog
Demian의 기술블로그
{ import Foundation }
NamS의 iOS일기

댓글남기기