관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. Diffable Data Source - UICollectionViewDiffableDataSource, UICollectionViewCompositionalLayout (컬렉션 뷰) 본문

iOS 응용 (swift)

[iOS - swift] 2. Diffable Data Source - UICollectionViewDiffableDataSource, UICollectionViewCompositionalLayout (컬렉션 뷰)

jake-kim 2021. 10. 2. 22:35

1. Diffable Data Source - UITableViewDiffableDataSource (테이블 뷰)

2. Diffable Data Source - UICollectionViewDiffableDataSource (컬렉션 뷰)

UICollectionViewDiffableDataSource

UICollectionViewCompositionalLayout

  • UICollectionViewLayout의 서브클래스이며 compositinoal하게 레이아웃을 쉽게 적용하기 위해서 등장

  • 개념 - compositinoal
    • collectionView의 레이아웃을 이루는 객체들 Item, Group, Section에 각각 레이아웃정보를 주고, 그 객체들을 합쳐서 구성

  • 사용방법: Item 안쪽에서부터, Group, Section 순으로 바깥쪽 레이아웃속성을 지정해주어서 적용
    • NSCollectionLayoutSize 생성 > Item, Group, Section 객체의 생성자에 적용
    • NSCollectionLayoutItem
    • NSCollectionLayoutGroup
    • NSCollectionLayoutSection
func createBasicListLayout() -> UICollectionViewLayout { 
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                  
                                         heightDimension: .fractionalHeight(1.0))    
    let item = NSCollectionLayoutItem(layoutSize: itemSize)  
  
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                          
                                          heightDimension: .absolute(44))    
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,                                                   
                                                     subitems: [item])  
  
    let section = NSCollectionLayoutSection(group: group)    

    let layout = UICollectionViewCompositionalLayout(section: section)    
    return layout
}
  • 크기 속성
    • absolute: 고정 크기
    • fractionalHeight, fractinoalWidth: 비율 크기
    • estimated: 최소 크기

예제) Diffable Data Source

// PhotoViewModel2.swift
var dataSource: UICollectionViewDiffableDataSource<Section, Photo>!

// PhotoViewController2.swift
viewModel.dataSource = UICollectionViewDiffableDataSource<Section, Photo>(collectionView: collectionView,
                                                                          cellProvider: { [weak self] collectionView, indexPath, photo in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCollectionViewCell.identifier, for: indexPath)
    self?.viewModel.loadImages(for: photo)
    (cell as? PhotoCollectionViewCell)?.model = photo

    return cell
})
  • ViewModel에서 snapshot으로 데이터 업데이트
// loadData() 메소드
var snapshot = weakSelf.dataSource.snapshot()
if snapshot.sectionIdentifiers.isEmpty {
    snapshot.appendSections([.main])
}
snapshot.appendItems(responseDTO.toDomain())
DispatchQueue.global(qos: .background).async {
    weakSelf.dataSource.apply(snapshot, animatingDifferences: false)
}

// loadImages() 메소드
photo.image = image
var snapshot = `self`.dataSource.snapshot()
guard snapshot.indexOfItem(photo) != nil else { return }

snapshot.reloadItems([photo])
DispatchQueue.global(qos: .background).async {
    `self`.dataSource.apply(snapshot, animatingDifferences: false)
}

예제) Compositional Layout - 왼쪽에 1개의 Item, 오른쪽에 3개의 Item

  • 준비 - customCollectionViewCell을 사용한다면, UIImageView의 속성 값 지정이 필요
photoImageView.contentMode = .scaleToFill
photoImageView.clipsToBounds = true
  • 2개의 group으로 이루어진 layout

  • 설계

  • 구현
    • Group은 총 2개 존재
    • 왼쪽 그룹은 item이 1개 존재
    • 오른쪽 그룹은 item이 3개 존재: item 3개 모두 크기가 동일하다고하면 item하나만 정의한다음 Group객체에 item객체 하나를 넣고 count:3으로 부여
// UICollectionViewLayout.swift

extension UICollectionViewLayout {
	// 이곳에다 구현
}

// 사용하는 곳
// PhotoViewController2.swift

class PhotoViewController2: UIViewController {
	private lazy var collectionView: UICollectionView = {
	    let view = UICollectionView(frame: view.bounds, collectionViewLayout: .leftThreeRightThree) // <- `.leftThreeRightThree`와 같이 사용
        ...
    }
}
  • 주의 사항
    • item의 fraction은 해당 item을 감싸고 있는 group에 의존받으므로, Cell의 콘텐트 사이즈를 변경하려면 group의 fraction값을 조정하여 변경해야 가능 (ex - item의 fraction을 0.3으로 주면 0.7만큼 그룹 내의 빈 공간이 생기는것 주의)
    • 각 item의 contentInsets값은 group의 생성자에 입력되기 전에 적용해주어야 적용되며, group이 만들어지고난 후에 적용하면 미적용
// leftOneRightThree

static let leftOneRightThree = UICollectionViewCompositionalLayout { section, environment in

    let margin = 1.0

    // 좌측 그룹
    let leadingItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                 heightDimension: .fractionalHeight(1.0))
    let leadingItem = NSCollectionLayoutItem(layoutSize: leadingItemSize)
    leadingItem.contentInsets = NSDirectionalEdgeInsets(top: margin,
                                                        leading: margin,
                                                        bottom: margin,
                                                        trailing: margin)

    let leadingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                                  heightDimension: .fractionalHeight(1.0))
    let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: leadingGroupSize,
                                                        subitem: leadingItem,
                                                        count: 1)

    // 우측 그룹
    let trailingItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                  heightDimension: .fractionalHeight(1.0))
    let trailingItem = NSCollectionLayoutItem(layoutSize: trailingItemSize)
    trailingItem.contentInsets = NSDirectionalEdgeInsets(top: margin,
                                                        leading: margin,
                                                        bottom: margin,
                                                        trailing: margin)

    let trailingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                                   heightDimension: .fractionalHeight(1.0))
    let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize,
                                                         subitem: trailingItem,
                                                         count: 3)

    // 좌측 그룹과 우측 그룹을 포함하는 그룹
    let containerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                    heightDimension: .fractionalHeight(0.6))
    let containerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: containerGroupSize,
                                                            subitems: [leadingGroup, trailingGroup])

    let section = NSCollectionLayoutSection(group: containerGroup)
    return section
}

예제) Compositional Layout - 왼쪽에 3개의 Item, 오른쪽에 3개의 Item

  • 2개의 그룹으로 이루어진 layout

  • 설계

  • item의 fraction height값이 모두 다르므로, item 6개 모두 생성하고 각 fraction height값은 위와 같이 각 group에 속하는 item fraction height 합이 1이 되도록 설계
static let leftThreeRightThree = UICollectionViewCompositionalLayout { section, environment in

        let margin = 2.0

        // 좌측 그룹

        /// first item
        let leadingFirstItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                          heightDimension: .fractionalHeight(0.35))
        let leadingFirstItem = NSCollectionLayoutItem(layoutSize: leadingFirstItemSize)
        leadingFirstItem.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)

        /// second item
        let leadingSecondItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                           heightDimension: .fractionalHeight(0.2))
        let leadingSecondItem = NSCollectionLayoutItem(layoutSize: leadingSecondItemSize)
        leadingSecondItem.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)

        /// third item
        let leadingThirdItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                           heightDimension: .fractionalHeight(0.45))
        let leadingThirdItem = NSCollectionLayoutItem(layoutSize: leadingThirdItemSize)
        leadingThirdItem.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)

        /// leading group
        let leadingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                                      heightDimension: .fractionalHeight(1.0))
        let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: leadingGroupSize,
                                                            subitems: [leadingFirstItem, leadingSecondItem, leadingThirdItem])

        // 우측 그룹
        /// first item
        let trailingFirstItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                           heightDimension: .fractionalHeight(0.5))
        let trailingFirstItem = NSCollectionLayoutItem(layoutSize: trailingFirstItemSize)
        trailingFirstItem.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)

        /// second item
        let trailingSecondItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                           heightDimension: .fractionalHeight(0.2))
        let trailingSecondItem = NSCollectionLayoutItem(layoutSize: trailingSecondItemSize)
        trailingSecondItem.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)

        /// third item
        let trailingThirdItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                           heightDimension: .fractionalHeight(0.3))
        let trailingThirdItem = NSCollectionLayoutItem(layoutSize: trailingThirdItemSize)
        trailingThirdItem.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)

        /// trailing group
        let trailingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5),
                                                       heightDimension: .fractionalHeight(1.0))
        let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize,
                                                             subitems: [trailingFirstItem, trailingSecondItem, trailingThirdItem])

        /// margin
        [trailingFirstItem, trailingSecondItem, trailingThirdItem].forEach {
            $0.contentInsets = NSDirectionalEdgeInsets(top: margin, leading: margin, bottom: margin, trailing: margin)
        }

        // 좌측 그룹과 우측 그룹을 포함하는 그룹
        let containerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                        heightDimension: .fractionalHeight(1.0))
        let containerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: containerGroupSize,
                                                                subitems: [leadingGroup, trailingGroup])

        let section = NSCollectionLayoutSection(group: containerGroup)
        return section
    }

dataSource를 비우고, cell에도 모두 초기화 시키는 방법

  • 빈 snapshot을 만든 후, 기존 dataSource에 apply 실행
let snapshot =  NSDiffableDataSourceSnapshot<Section, Photo>.init()
dataSource.apply(snapshot)

* 전체 코드: https://github.com/JK0369/PaginationExample

 

* 참고

- UICollectionViewDiffableDataSource: https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource

- Implementing Modern Collection Views: 

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

- Compositional Layout: 

https://www.raywenderlich.com/5436806-modern-collection-views-with-compositional-layouts

- UICollectionViewCompositionalLayout

https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout

Comments