본문 바로가기

Xcode 개발

[Xcode] 새로운 방법으로 컬렉션뷰 만들기(3) - Diffable Data Source + List Configuration

이번 게시글에서는 가장 최신 기능 조합인

Diffable Data Source(데이터 관리)와
List Configuration(컬렉션뷰 레이아웃)을

사용하여 컬렉션뷰를 만들어보자


 

Diffable Data Source 

: UICollectionViewDataSource를 상속받은 UICollectionViewDiffableDataSource

 

2019년 iOS 13 WWDC19 참고 (advances in UI data sources)

 

Advances in UI Data Sources - WWDC19 - Videos - Apple Developer

Use UI Data Sources to simplify updating your table view and collection view items using automatic diffing. High fidelity, quality...

developer.apple.com

 

기존과 달라진 점

- indexpath 안씀 -> itemIdentifier로 대체

- cellForRow 안씀

- numberOfCell 안씀

- reloadData 안씀 -> apply() : 변경되는 데이터 양의 상관없이 백그라운드의 스레드에서 연산처리

- 데이터는 각각의 고유한 모델 사용(Hashable을 채택한 model 사용)

 

다음의 기능을 코드로 구현해보자.

1. 간단한 리스트를 컬렉션뷰로 나타내기

2. 서치바에 글자 입력 후 엔터 시, 컬렉션뷰에 추가해서 보여주기

3. 셀 클릭 시, 어럴트 창 렌더링하기

class DiffableCollectionViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var searchBar: UISearchBar!
    
    // 생략
    // var cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, String>!
    
    // <Int, String> = <섹션 정보, 모델 타입>
    private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
    
    var list = ["이순신", "김좌진", "맥아더", "노르망디"]
    
    override func viewDidLoad() {
        super.viewDidLoad()

        searchBar.delegate = self
        
        collectionView.collectionViewLayout = createLayout()
        collectionView.delegate = self
        
        configureDataSource()
    }

}

extension DiffableCollectionViewController {
    
    private func createLayout() -> UICollectionViewLayout {
        var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let layout = UICollectionViewCompositionalLayout.list(using: configuration)
        return layout
    }
    
    private func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String>(handler: { cell, indexPath, itemIdentifier in
            var contentConfig = UIListContentConfiguration.valueCell()
            contentConfig.text = itemIdentifier
            contentConfig.secondaryText = "\(itemIdentifier.count)"
            cell.contentConfiguration = contentConfig
            
            var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
            backgroundConfig.strokeWidth = 2
            backgroundConfig.strokeColor = .brown
            cell.backgroundConfiguration = backgroundConfig
        })
        
        // collectionView.dataSource = self
        // cellForItem, numberOfItems 대체
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
            let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
            return cell
        })
        
        // 1. 초기 리스트 보여주기
        // Initial Snapshot
        var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
        snapshot.appendSections([0])
        snapshot.appendItems(list)
        dataSource.apply(snapshot)
    }
}

extension DiffableCollectionViewController: UICollectionViewDelegate {
    
    // 3. 셀 클릭 시, 어럴트 창 렌더링하기
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
                
        let alert = UIAlertController(title: item, message: "클릭!", preferredStyle: .alert)
        let ok = UIAlertAction(title: "확인", style: .default)
        alert.addAction(ok)
        present(alert, animated: true)
    }
}

extension DiffableCollectionViewController: UISearchBarDelegate {
    
    // 2. 서치바에 단어 입력 후 엔터 시, 컬렉션뷰에 추가하기
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        
        var snapshot = dataSource.snapshot()
        snapshot.appendItems([searchBar.text!])
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

1. 초기 UICollectionViewLayout와 CellRegistration 설정은 이전과 동일 (Extension 활용하여 메서드 추가)

    - 다만, 이 과정에서 cellRegistration을 전역변수로 지정할 필요가 없어져서 생략

    - 전역변수를 지우면 메서드 내부의 cellRegistration에서 타입 오류 발생

    - let cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, String> 

    - 또는 위 코드처럼 해결

2. dataSource 전역변수 선언 후, Extension 내부 메서드를 통해 cellForRow/numberOfItems 매서드를 대체

    - UICollectionViewDiffableDataSource 타입 할당 (개발 팁: 클로져 매개변수 부분에서 Enter)

3. Snapshot을 통한 데이터 관리

    - 우선, 기존의 snapshot을 가져오고,

    - snapshot에 저장된 데이터를 변경하고,

    - apply하기 -> 여기에서 기존의 snapshot과 비교를 통해 레이아웃에 적용

 

 

주의사항

- 스토리보드의 Scene 상위 아이콘 목록에서 iOS 14 버전 이상에서 Search Display Controller를 사용하려면 복잡하므로 삭제하고 진행

- didSelect 같은 매서드는 UICollectionViewDelegate로부터 여전히 사용해야함

- Snapshot 기능을 사용할 때, 모델이 담긴 리스트는 변경이 되지 않음

 

 

마지막으로, 이전 게시물에서 구현했던

UICollectionViewDataSource + List Configuration 코드를

Diffable Data Source + List Configuration 코드로 개선해보자. (편의상 viewDidLoad에 모두 구현)

struct User: Hashable {
    let id = UUID().uuidString
    
    let name: String
    let age: Int
}

class TestCollectionViewController: UICollectionViewController {

    var list: [User] = [
        User(name: "태이슨", age: 25),
        User(name: "태이슨", age: 25),
        User(name: "제이슨", age: 33),
        User(name: "구스타보", age: 5),
    ]
    
    var cellRegistration: UICollectionView.CellRegistration<UICollectionViewListCell, User>!
    
    var dataSource: UICollectionViewDiffableDataSource<Int, User>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        configuration.showsSeparators = false
        configuration.backgroundColor = .brown
        
        let layout = UICollectionViewCompositionalLayout.list(using: configuration)
        
        collectionView.collectionViewLayout = createLayout()
        
        cellRegistration = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in
            
            // UIContentConfiguration 프로토콜 => Cell ContentView
            
            // contentConfiguration 채택 => label/img 관리
            var content = UIListContentConfiguration.valueCell() //cell.defaultContentConfiguration()
            
            content.text = itemIdentifier.name
            content.textProperties.color = .red
            content.secondaryText = "\(itemIdentifier.age)살"
            content.prefersSideBySideTextAndSecondaryText = false
            content.textToSecondaryTextVerticalPadding = 20
            content.image = itemIdentifier.age < 8 ? UIImage(systemName: "person.fill") : UIImage(systemName: "star")
            content.imageProperties.tintColor = .yellow
            
            cell.contentConfiguration = content
            
            // backgroundConfiguration 채택 => 그림자/배경 관리
            var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
            
            backgroundConfig.backgroundColor = .lightGray
            backgroundConfig.cornerRadius = 10
            backgroundConfig.strokeWidth = 2
            backgroundConfig.strokeColor = .systemPink
            
            cell.backgroundConfiguration = backgroundConfig
        }
        
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
            let cell = collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: itemIdentifier)
            return cell
        })
        
        // Initial Snapshot
        var snapshot = NSDiffableDataSourceSnapshot<Int, User>()
        snapshot.appendSections([0])
        snapshot.appendItems(list)
        dataSource.apply(snapshot)
    }
    
//    생략
//    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//        return list.count
//    }

//    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//        let item = list[indexPath.item]
//        let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
//        return cell
//    }
}

1. numberOfItems와 cellForItemAt 메서드 삭제

2. 사용하는 User 모델(Struct)의 Hashable 프로토콜 채택

    - Class로 Hashable을 채택하면 Equatable까지 채택해야함 -> 복잡해짐 -> 보통 Struct로 구현

    - UUID 속성은 초기화를 통해 애플이 고유한 값을 제공 -> 각 모델의 Hashable 프로토콜 충족 

    - name과 age 속성이 겹쳐도 UUID로 인해 오류가 나지 않음

 

[Xcode] 새로운 방법으로 컬렉션뷰 만들기(2) - UICollectionViewDataSource + List Configuration 코드 개선

 

[Xcode] 새로운 방법으로 컬렉션뷰 만들기(2) - UICollectionViewDataSource + List Configuration 코드 개선

이전 게시글에서 UICollectionViewDataSource + List Configuration 조합으로 새로운 방식으로 컬렉션뷰를 만들어 보았다. 여기에서 코드를 더 개선해보자 [Xcode] 새로운 방법으로 컬렉션뷰 만들기(1) - UICollecti

adeulnom.tistory.com