[UIKit] Modern Collection View: Edit with Diffable DataSource

Junyoung Park·2022년 12월 6일
0

UIKit

목록 보기
114/142
post-thumbnail
post-custom-banner

Modern Collection View [4] - Edit with Diffable Data Source | Reordering | Swipe Action | Checkmark

Modern Collection View: Edit with Diffable DataSource

구현 목표

  • Diffable DataSource를 적용한 컬렉션 뷰의 편집 기능(스와이프 액션, 삭제 등) 적용

구현 태스크

  • 셀 액세서리 적용
  • 편집 기능 중 삭제 기능 구현
  • 스와이프 삭제 기능 구현
  • 드래그 드롭을 통한 셀 재정렬 기능 구현

핵심 코드

private var selectedCharacters = Set<Character>()
  • 체크 마크를 표시용으로 선택한 셀을 담아둘 전역 변수 집합 변수
if self.selectedCharacters.contains(character) {
                    accessories.append(.checkmark(displayed: .whenNotEditing))
                }
  • 셀 UI를 그릴 때 해당 셀이 집합에 포함되어 있다면 이전에 체크된 것이라는 뜻
collectionView.delegate = self
...

extension CharacterListViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
        if selectedCharacters.contains(item) {
            selectedCharacters.remove(item)
        } else {
            selectedCharacters.insert(item)
        }
        
        var snapshot = dataSource.snapshot()
        snapshot.reloadItems([item])
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}
  • 델리게이트 함수 중 특정 셀을 선택했을 때 호출되는 함수를 사용해 액세서리를 적용
  • 전역 변수 내 집합 삽입 및 삭제를 표시, 새롭게 스냅샷을 그린 뒤 해당 아이템만을 리로드하기
  • 스냅샷에게 던져주는 아이템은 배열임에 주의
override func setEditing(_ editing: Bool, animated: Bool) {
        super.setEditing(editing, animated: animated)
        collectionView.isEditing = editing
    }
  • 뷰 컨트롤러의 편집 세팅 함수를 통해 컬렉션 뷰의 편집 여부를 확인
navigationItem.leftBarButtonItem = editButtonItem
  • editButtonItem을 통해 해당 setEditing 여부를 토글
var accessories: [UICellAccessory] = [
                    .delete(displayed: .whenEditing, actionHandler: {
                        self.deleteCharacter(character)
                    }),
                ]
  • 셀 액세서리 여부에 체크 마크 이외에도 기본적으로 삭제 기능 제공
  • whenEditing을 통해 어느 상황에서 해당 액세서리를 보여줄지 결정 가능
  • 액션 핸들러에서 해당 삭제 버튼이 클릭되었을 때 실행될 함수 호출
private func deleteCharacter(_ character: Character) {
        guard let indexPath = dataSource.indexPath(for: character) else { return }
        backingStore[indexPath.section].characters.remove(at: indexPath.item)
        selectedCharacters.remove(character)
        var snapshot = dataSource.snapshot()
        snapshot.deleteItems([character])
        dataSource.apply(snapshot, animatingDifferences: true)
    }
  • 체크 마크를 표시할 셀 클릭 이벤트에서 실행되는 함수와 마찬가지로 deleteCharacter를 통해 삭제된 캐릭터를 전역 변수에서 제거 및 스냅샷에서 제거한 컬렉션 뷰 UI를 그리는 메소드 실행
    private lazy var listLayout: UICollectionViewLayout = {
        var listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        listConfig.headerMode = .supplementary
        listConfig.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath -> UISwipeActionsConfiguration? in
            guard let character = self?.dataSource.itemIdentifier(for: indexPath) else { return nil }
            return UISwipeActionsConfiguration(actions: [.init(style: .destructive, title: "Delete", handler: { [weak self] _, _, completion in
                self?.deleteCharacter(character)
                completion(true)
            })])
        }
        return UICollectionViewCompositionalLayout.list(using: listConfig)
    }()
  • 컬렉션 뷰 리스트 레이아웃 내 제공하는 스와이프 기능을 통해서도 삭제 기능 구현 가능
  • UICollectionLayoutListConfiguration의 스와이프 액션 핸들러를 통해 위의 삭제 메소드를 호출하는 컴플리션 사용
var accessories: [UICellAccessory] = [
                    .reorder(displayed: .whenEditing)
                ]
  • 액세서리의 reorder 프로퍼티를 통해 편집 중인 상황에서 특정 셀을 드래그 드롭으로 재정렬할 수 있는 방식 제공
dataSource.reorderingHandlers.canReorderItem = { [weak self] character -> Bool in
            return (self?.navigationItem.searchController?.searchBar.text ?? "").isEmpty
        }
  • 해당 재정렬 기능은 데이터 소스의 reorderingHandlers가 제공하는 여러 가지 프로퍼티를 통해 조작 가능
  • canReorderItem을 통해 어떤 상황에 재정렬이 가능한지 결정 가능
  • 위 뷰에서는 검색 바의 텍스트가 없을 때에만 재정렬이 가능하도록 설정
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
            guard let self = self else { return }
            var backingStore = self.backingStore
            for sectionTransaction in transaction.sectionTransactions {
                let sectionIdentifier = sectionTransaction.sectionIdentifier
                if let sectionIndex = transaction.finalSnapshot.indexOfSection(sectionIdentifier) {
                    backingStore[sectionIndex].characters = sectionTransaction.finalSnapshot.items
                }
            }
            self.backingStore = backingStore
            self.reloadHeaders()
        }
  • didReorder는 해당 재정렬 드래그 드롭이 실행이 된 이후 들어오는 스냅샷을 통해 실제 컬렉션 뷰에 어떻게 데이터 현황을 보고할지 결정하는 파트
  • 데이터 소스가 가지고 있는 데이터의 섹션 및 아이템 순서 모두가 변경되었을 수 있기 때문에 finalSnapshotindexOfSection을 통해 해당 아이템이 어떤 섹션에 들어있는지 체크, 현재 들고 있는 전역 변수 섹션에 새롭게 정렬된 아이템을 그 순서대로 넣고, 그대로 스냅샷을 전역 변수에 반영
  • 헤더 타이틀은 현재 섹션 내 아이템 개수 등을 표시하므로 새롭게 표시하는 함수 호출

구현 화면

해당 강의 시리즈 중 가장 궁금했고, 가장 도움이 되는 영상이었다! Diffable DataSource를 잘 사용만 한다면 큰 무기가 될 것이다!

profile
JUST DO IT
post-custom-banner

0개의 댓글