[Conference] Modern Collection Views by example

Junyoung Park·2022년 11월 21일
0

Conference

목록 보기
2/5
post-thumbnail

Modern Collection Views by example - Gaetano Matonti - Swift Heroes 2022

Modern Collection Views by example

컬렉션 뷰

  • List: UITableView 상속, Appearance, Expandable and collapsible sections

Diffable DataSource & Snapshot

  • 해시 값을 따르는 컬렉션 뷰 내 데이터 사용
  • 전후 스냅샷 비교, 달라진 아이템만을 연산 후 조정(추가, 삭제, 변경 등)

구현 예시 1

import Foundation
import UIKit

enum Section: Int, CaseIterable {
    case locations
    case favorites
}

enum Item: Hashable {
    case header(String)
    case item(String)
}

class ListDataSource: UICollectionViewDiffableDataSource<Section, Item> {
    convenience init(collectionView: UICollectionView) {
        let headerRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, _, item in
            guard case let .header(title) = item else { return }
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = title
            cell.contentConfiguration = contentConfiguration
            cell.accessories = [.outlineDisclosure()]
        }
        
        let itemRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, _, item in
            guard case let .item(title) = item else { return }
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = title
            cell.contentConfiguration = contentConfiguration
            cell.accessories = [.outlineDisclosure()]
        }
        
        self.init(collectionView: collectionView) { collectionView, indexPath, item in
            switch item {
            case .header:
                return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: item)
            case .item:
                return collectionView.dequeueConfiguredReusableCell(using: itemRegistration, for: indexPath, item: item)
            }
        }
    }
}
  • 컬렉션 뷰 리스트 셀 구성을 사용하는 헤더 및 아이템
import UIKit

class ListViewController: UICollectionViewController {
    private lazy var dataSource = ListDataSource(collectionView: collectionView)
    
    convenience init() {
        var listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar)
        listConfiguration.headerMode = .firstItemInSection
        let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        self.init(collectionViewLayout: layout)
        applyInitialSnapshot()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "List"
    }
    
    private func applyInitialSnapshot() {
        var locationSectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        let locationHeaderItem: Item = .header("Locations")
        locationSectionSnapshot.append([locationHeaderItem])
        locationSectionSnapshot.append(
            [
                .item("Mac"),
                .item("iPhone"),
                .item("iPad")
            ],
            to: locationHeaderItem
        )
        var favoriteSectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        let favoriteHeaderItem: Item = .header("Favorites")
        favoriteSectionSnapshot.append([favoriteHeaderItem])
        favoriteSectionSnapshot.append(
            [
                .item("Docs"),
                .item("iCloud")
            ],
            to: favoriteHeaderItem
        )
        
        dataSource.apply(locationSectionSnapshot, to: .locations)
        dataSource.apply(favoriteSectionSnapshot, to: .favorites)
    }
}
  • UICollectionViewCompositionalLayout이 제공하는 list 프로퍼티를 통해 레이아웃 구성
  • Diffable DataSource를 구현하기 위한 스냅샷 적용
  • 스냅샷을 적용할 때 헤더 및 아이템을 별도로 구현
  • 해당 헤더 별로 여러 아이템을 별도로 추가
  • 각 섹션 별로 아이템 적용

구현 예시 2

import UIKit

class AppStoreViewController: UICollectionViewController {
    private var models: [String] = Array(repeating: "Lorem Ipsum", count: 100)
    convenience init() {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(64))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4)
        item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: .fixed(8), trailing: nil, bottom: .fixed(8))
        
        let groupItemCount = 3
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension: .estimated(itemSize.heightDimension.dimension * CGFloat(groupItemCount)))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, repeatingSubitem: item, count: groupItemCount)
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .groupPagingCentered
        let layout = UICollectionViewCompositionalLayout(section: section)
        self.init(collectionViewLayout: layout)
        title = "App"
        collectionView.register(AppStoreCollectionViewCell.self, forCellWithReuseIdentifier: AppStoreCollectionViewCell.identifier)
    }
}

extension AppStoreViewController {
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AppStoreCollectionViewCell.identifier, for: indexPath) as? AppStoreCollectionViewCell else { fatalError() }
        let model = models[indexPath.row]
        cell.configure(with: model)
        return cell
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return models.count
    }
}
  • CompositionalLayout을 통해 아이템, 그룹, 섹션 간의 복잡한 레이아웃 가능
  • 앱 스토어와 같은 구성은 세 개의 세로 아이템이 한 개의 그룹으로 섹션을 구성, 페이징을 넘길 때 그룹 단위로 넘어가는 구성
  • 가데이터 100개를 넣어 컬렉션 뷰 구성

import UIKit

class AppStoreCollectionViewCell: UICollectionViewCell {
    static let identifier = "AppStoreCollectionViewCell"
    private let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.clipsToBounds = true
        imageView.contentMode = .scaleAspectFill
        imageView.layer.cornerRadius = 12
        imageView.backgroundColor = .label
        return imageView
    }()
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.textColor = .label
        label.font = .systemFont(ofSize: 18, weight: .bold)
        label.numberOfLines = 1
        return label
    }()
    private let subtitleLabel: UILabel = {
        let label = UILabel()
        label.textColor = .darkGray
        label.font = .systemFont(ofSize: 15, weight: .semibold)
        return label
    }()
    private let downloadButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "icloud.and.arrow.down"), for: .normal)
        return button
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setUI() {
        contentView.addSubview(imageView)
        contentView.addSubview(titleLabel)
        contentView.addSubview(downloadButton)
        contentView.addSubview(subtitleLabel)
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
        downloadButton.translatesAutoresizingMaskIntoConstraints = false
        
        imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
        imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
        imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true
        imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
        
        downloadButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true
        downloadButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
        downloadButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
        downloadButton.widthAnchor.constraint(equalTo: downloadButton.heightAnchor).isActive = true
        
        titleLabel.topAnchor.constraint(equalTo: imageView.topAnchor).isActive = true
        titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10).isActive = true
        titleLabel.trailingAnchor.constraint(equalTo: downloadButton.leadingAnchor, constant: -16).isActive = true
        
        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5).isActive = true
        subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor).isActive = true
        subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor).isActive = true
        
    }
    
    func configure(with model: String) {
        titleLabel.text = model
        subtitleLabel.text = model
    }
}
  • 커스텀 셀 구현

Diffable DataSource, CompositionalLayout 등은 iOS 13 이후부터 지원되는 다소 '핫'한 스택이지만, 그 이름처럼 기존 컬렉션 뷰에 비해 생각해야 할 부분이 많다. 어렵지만 그 이름처럼 '모던'한 형태를 구현하려면 필수다. 비록 컨퍼런스 영상을 이해하기에는 아직 벅찼지만 찬찬히 공부해보면서 다듬어보자.

profile
JUST DO IT

0개의 댓글