[UIKit] CollectionViewCell 선택하여 삭제하는 법

jess·2023년 4월 12일
0

[iOS]

목록 보기
4/9

1. PhotoCollectionViewCell

  • 상태에 따라 변화값을 준다.
 override var isHighlighted: Bool {
        didSet {
            highlightIndicator.isHidden = !isHighlighted
        }
    }

    override var isSelected: Bool {
        didSet {
            highlightIndicator.isHidden = !isSelected
            selectIndicator.isHidden = !isSelected
        }
    }

2. PhotoViewController

  • 기본 모드, 선택모드 나누어 로직을 구현한다.
// MARK: - Selected Button
    
    enum Mode {
        case view
        case select
    }
    
// UICollecionView에서 선택한 셀의 IndexPath를 Key로,
// 선택 여부를 value로 가지며, 선택한 셀들의 정보를 저장한다. 
    var dictionarySelectedIndexPath: [IndexPath : Bool] = [:]
    
    var eMode: Mode = .view {
        didSet {
            switch eMode {

// 선택 모드에서 뷰 모드로 변경할 때, 선택한 셀들을 모두 선택 해제하고 선택된 셀의 indexPath를 저장하는 딕셔너리를 비우고, 다시 선택할 수 없는 단일 선택 모드로 변경해주는 역할을 한다. 
            case .view:
                for (key, value) in dictionarySelectedIndexPath {
                    if value {
                        collectionView.deselectItem(at: key, animated: true)
                    }
                }
                dictionarySelectedIndexPath.removeAll()
                collectionView.allowsMultipleSelection = false
                
                
            case .select:
                let selectButton = UIButton(type: .custom)
                selectButton.setImage(UIImage(named: "canceledButton")?.withRenderingMode(.alwaysOriginal), for: .normal)
                selectButton.addTarget(self, action: #selector(didCanceledButtonClicked), for: .touchUpInside)
                let selectBarButtonItem = UIBarButtonItem(customView: selectButton)
                
                let addButton = UIButton(type: .custom)
                addButton.setImage(UIImage(named: "deletedButtonDisabled")?.withRenderingMode(.alwaysOriginal), for: .normal)
                addButton.addTarget(self, action: #selector(didDeleteButtonClicked), for: .touchUpInside)
                let addBarButtonItem = UIBarButtonItem(customView: addButton)
                
                navigationItem.rightBarButtonItems = [addBarButtonItem, selectBarButtonItem]

// collectionView에서 다중 선택이 가능하도록 설정
                collectionView.allowsMultipleSelection = true
            }
        }
    }

3. Buttons 구현

선택버튼

let selectButton = UIButton(type: .custom)
                selectButton.setImage(UIImage(named: "canceledButton")?.withRenderingMode(.alwaysOriginal), for: .normal)
                selectButton.addTarget(self, action: #selector(didCanceledButtonClicked), for: .touchUpInside)
                let selectBarButtonItem = UIBarButtonItem(customView: selectButton)

추가버튼

let addButton = UIButton(type: .custom)
                addButton.setImage(UIImage(named: "deletedButtonDisabled")?.withRenderingMode(.alwaysOriginal), for: .normal)
                addButton.addTarget(self, action: #selector(didDeleteButtonClicked), for: .touchUpInside)
                let addBarButtonItem = UIBarButtonItem(customView: addButton)

취소버튼 (선택버튼이 클릭되면, 취소버튼이 나오도록 설정)

let selectButton = UIButton(type: .custom)
                selectButton.setImage(UIImage(named: "canceledButton")?.withRenderingMode(.alwaysOriginal), for: .normal)
                selectButton.addTarget(self, action: #selector(didCanceledButtonClicked), for: .touchUpInside)
                let selectBarButtonItem = UIBarButtonItem(customView: selectButton)

삭제버튼

lazy var deleteBarButton : UIBarButtonItem = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(named: "deletedButtonGrey")?.withRenderingMode(.alwaysOriginal), for: .normal)
        button.addTarget(self, action: #selector(didDeleteButtonClicked), for: .touchUpInside)
        return UIBarButtonItem(customView: button)
    }()

📝 UIBarButton으로 구현하지 않고, UIButton으로 구현한 이유

  • UIBarButton으로 구현하니 버튼 간 간격을 좁히기 힘들었다. UIButton으로 구현하니, 내가 원하는 정도의 간격을 조정할 수 있었다.
  • 여기 참고

4. 함수 구현

  • 각각 버튼에 맞는 함수를 구현한다.
  • 삭제 버튼 터치 시, 삭제하기 위해 선택한 셀을 찾아 Realm에서 삭제하고 reloadData한다.

삭제버튼

@objc func didDeleteButtonClicked(_ sender: UIBarButtonItem) {
        
        // 선택된 사진이 없으면 실행하지 않음
        guard !dictionarySelectedIndexPath.isEmpty else {
            return
        }
        
        let alert = UIAlertController(title: nil, message: "삭제하시겠습니까?", preferredStyle: .alert)
        
        let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
        alert.addAction(cancelAction)
        
        let deleteAction = UIAlertAction(title: "삭제", style: .destructive) { [self] _ in
            
            var deleteNeededIndexPaths: [IndexPath] = []
            print(deleteNeededIndexPaths.isEmpty)
            for (key, value) in self.dictionarySelectedIndexPath {
                if value {
                    deleteNeededIndexPaths.append(key)
                }
            }
            // realm 데이터 삭제
            **for indexPath in deleteNeededIndexPaths.sorted(by: { $0.item > $1.item }) {
                guard let photoData = self.viewModel.photoData(at: indexPath) else { return }
                self.realmManager.delete(photoData: photoData)
            }
            // colletionViewCell 삭제
            self.collectionView.deleteItems(at: deleteNeededIndexPaths)
            self.dictionarySelectedIndexPath.removeAll()
            self.collectionView.reloadData()**
            
            // 선택 모드 해제
            updateViewWithButtons()
            
        }
        alert.addAction(deleteAction)
        present(alert, animated: true, completion: nil)
        
    }
  • 데이터 삭제 코드 자세히 설명
    // 선택된 셀의 IndexPath를 담은 배열인 deleteNeededIndexPaths를 정렬하여, 삭제해야 할 셀의 인덱스를 큰 값부터 작은값으로 정렬한다.
    // 그런 다음 반복문을 사용하여, 삭제해야 할 IndexPath에 대한 작업을 수행한다. 
    for indexPath in deleteNeededIndexPaths.sorted(by: { $0.item > $1.item }) {
                    guard let photoData = self.viewModel.photoData(at: indexPath) else { return }
                    self.realmManager.delete(photoData: photoData)
                }
                // colletionViewCell 삭제
                self.collectionView.deleteItems(at: deleteNeededIndexPaths)
                self.dictionarySelectedIndexPath.removeAll()
                self.collectionView.reloadData()
                
                // 선택 모드 해제
                updateViewWithButtons()
                
            }

추가버튼

@objc func addButtonTapped(_ sender: Any) {
        let photoDetailVC = PhotoDetailViewController()
        navigationController?.pushViewController(photoDetailVC, animated: true)
    }

선택버튼

@objc func didSelectButtonClicked(_ sender: UIBarButtonItem) {
        if eMode == .select {
            eMode = .view
            collectionView.alpha = 1.0
            if dictionarySelectedIndexPath.isEmpty {
                deleteBarButton.isEnabled = false
            }
        } else {
            eMode = .select
            collectionView.alpha = 0.7
            deleteBarButton.isEnabled = true
        }
    }

취소버튼

@objc func didCanceledButtonClicked(_ sender: UIBarButtonItem) {
        updateViewWithButtons()
    }
  • 같은 클래스 내 여러번 호출되어서, updateViewWithButtons() 함수로 묶었다.
private func updateViewWithButtons() {
        eMode = .view
        let addButton = UIButton(type: .custom)
        addButton.setImage(UIImage(named: "addButton")?.withRenderingMode(.alwaysOriginal), for: .normal)
        addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
        let addBarButtonItem = UIBarButtonItem(customView: addButton)
        
        let selectButton = UIButton(type: .custom)
        selectButton.setImage(UIImage(named: "selectedButton")?.withRenderingMode(.alwaysOriginal), for: .normal)
        selectButton.addTarget(self, action: #selector(didSelectButtonClicked), for: .touchUpInside)
        let selectBarButtonItem = UIBarButtonItem(customView: selectButton)
        
        let space = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
        space.width = 10
        
        navigationItem.rightBarButtonItems = [addBarButtonItem, space, selectBarButtonItem]
        
        collectionView.alpha = 1.0
        collectionView.reloadData()
        
    }

삭제버튼

@objc func didDeleteButtonClicked(_ sender: UIBarButtonItem) {
        
        // 선택된 사진이 없으면 실행하지 않음
        guard !dictionarySelectedIndexPath.isEmpty else {
            return
        }
        
        let alert = UIAlertController(title: nil, message: "삭제하시겠습니까?", preferredStyle: .alert)
        
        let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
        alert.addAction(cancelAction)
        
        let deleteAction = UIAlertAction(title: "삭제", style: .destructive) { [self] _ in
            
            var deleteNeededIndexPaths: [IndexPath] = []
            print(deleteNeededIndexPaths.isEmpty)
            for (key, value) in self.dictionarySelectedIndexPath {
                if value {
                    deleteNeededIndexPaths.append(key)
                }
            }
            // realm 데이터 삭제
            for indexPath in deleteNeededIndexPaths.sorted(by: { $0.item > $1.item }) {
                guard let photoData = self.viewModel.photoData(at: indexPath) else { return }
                self.realmManager.delete(photoData: photoData)
            }
            // colletionViewCell 삭제
            self.collectionView.deleteItems(at: deleteNeededIndexPaths)
            self.dictionarySelectedIndexPath.removeAll()
            self.collectionView.reloadData()
            
            // 선택 모드 해제
            updateViewWithButtons()
            
        }
        alert.addAction(deleteAction)
        present(alert, animated: true, completion: nil)
        
    }
  • 사용자가 확인을 누르면 선택된 사진들을 삭제한다.
  • 이 때, dictionarySelectedIndexPath 딕셔너리에서 삭제가 필요한 인덱스들을 deleteNeededIndexPaths 배열에 저장한다.
  • 저장된 인덱스들을 역순으로 정렬하여 deleteNeededIndexPaths 배열에 있는 각 인덱스에 해당하는 셀들을 삭제한다.
  • 선택된 사진들을 딕셔너리에서 삭제하고, collecionView를 reload한다.
  • updateViewWithButtons() 메서드를 호출하여 선택 모드를 해제한다.

결과

  • 깔끔하게 삭제됨 ~

⚠️ 선택 해제 했음에도 불구하고, 사진이 삭제되는 오류가 발생했다.

func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
        self.dictionarySelectedIndexPath[indexPath] = false // 선택을 해제한 셀의 IndexPath를 딕셔너리에서 제거
    }
  • 선택을 해제한 IndexPath를 확인하고, 딕셔너리에서 제거해주는 코드를 추가하려면 해당 IndexPath를 추적할 수 있는 방법이 필요
  • 일반적으로, Collection View에서 셀의 선택 상태를 추적하는 방법은 UICollectionViewDelegate프로토콜의 collectionView(_:didDeselectItemAt:)메서드를 구현하는 것이다.. 이 메서드는 Collection View에서 셀을 선택 해제할 때마다 호출
  • 따라서, collectionView(_:didDeselectItemAt:)메서드를 활용하여 선택을 해제한 IndexPath를 추적하고, 딕셔너리에서 제거하는 코드를 추가

0개의 댓글