관리 메뉴

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

[iOS - swift] DragDropCollectionView 프레임워크 본문

iOS framework

[iOS - swift] DragDropCollectionView 프레임워크

jake-kim 2022. 1. 14. 22:09

 

준비

  • 코드로 UI를 편리하게 작성하기 위해서 Then, SnapKit 프레임워크 설치
      pod 'Then'
      pod 'SnapKit'​
      pod 'Reusable'

사용 방법

  • 의존성 도구 없이 DragDropCollectionView 파일을 복사 붙여넣기하여 사용
    (아래 github에 명시)

https://github.com/Lior539/DragDropCollectionView

  • DragDropCollectionView.swift을 리펙토링하여 아래처럼 사용
    //
    //  DragDropCollectionView.swift
    //  DragDrop
    //
    //  Created by Lior Neu-ner on 2014/12/30.
    //  Copyright (c) 2014 LiorN. All rights reserved.
    // 3rd test for git submodule
    //Just testing git subtree for the second time
    
    import UIKit
    import AVFoundation
    
    protocol DrapDropCollectionViewDelegate: AnyObject {
      func dragDropCollectionViewDidMoveCellFromInitialIndexPath<T>(
        _ collectionView: DragDropCollectionView<T>,
        initialIndexPath: IndexPath,
        toNewIndexPath newIndexPath: IndexPath
      )
      func dragDropCollectionViewDraggingDidBeginWithCellAtIndexPath<T>(_ collectionView: DragDropCollectionView<T>, indexPath: IndexPath)
      func dragDropCollectionViewDraggingDidEndForCellAtIndexPath<T>(_ collectionView: DragDropCollectionView<T>, indexPath: IndexPath)
    }
    
    class DragDropCollectionView<DraggableCellType>: UICollectionView,
      UIGestureRecognizerDelegate
      where DraggableCellType: UICollectionViewCell {
      weak var draggingDelegate: DrapDropCollectionViewDelegate?
      
      var longPressRecognizer = UILongPressGestureRecognizer().then {
        $0.delaysTouchesBegan = false
        $0.cancelsTouchesInView = false
        $0.numberOfTouchesRequired = 1
        $0.minimumPressDuration = 0.5
        $0.allowableMovement = 10.0
      }
      
      var draggedCellIndexPath: IndexPath?
      var draggingView: UIView?
      var touchOffsetFromCenterOfCell: CGPoint?
      let pingInterval = 0.03
      var isSwapEnabled = true
      
      required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
      }
      
      override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        self.commonInit()
      }
      
      func commonInit() {
        self.longPressRecognizer.addTarget(self, action: #selector(self.handleLongPress(_:)))
        self.longPressRecognizer.isEnabled = false
        self.addGestureRecognizer(self.longPressRecognizer)
      }
      
      @objc func handleLongPress(_ longPressRecognizer: UILongPressGestureRecognizer) {
        let touchLocation = longPressRecognizer.location(in: self)
        
        switch longPressRecognizer.state {
        case .began:
          self.draggedCellIndexPath = self.indexPathForItem(at: touchLocation)
          guard
            let draggedCellIndexPath = self.draggedCellIndexPath,
            self.cellForItem(at: draggedCellIndexPath) is DraggableCellType,
            case _ = self.draggingDelegate?.dragDropCollectionViewDraggingDidBeginWithCellAtIndexPath(self, indexPath: draggedCellIndexPath),
            let draggedCell = self.cellForItem(at: draggedCellIndexPath)
          else { return }
          let draggingView = UIImageView(image: self.getRasterizedImageCopyOfCell(draggedCell))
          self.draggingView = draggingView
          draggingView.center = (draggedCell.center)
          self.addSubview(draggingView)
          draggedCell.isHidden = true
          self.touchOffsetFromCenterOfCell = CGPoint(x: draggedCell.center.x - touchLocation.x, y: draggedCell.center.y - touchLocation.y)
          UIView.animate(
            withDuration: 0.4,
            animations: {
              draggingView.transform = .init(scale: 1.1)
              draggingView.alpha = 0.9
              draggingView.layer.shadowRadius = 20
              draggingView.layer.shadowColor = UIColor.Slide.nero.cgColor
              draggingView.layer.shadowOpacity = 0.2
              draggingView.layer.shadowOffset = CGSize(width: 0, height: 25)
            }
          )
        case .changed:
          guard
            self.draggedCellIndexPath != nil,
            let touchOffsetFromCenterOfCell = self.touchOffsetFromCenterOfCell
          else { return }
          self.draggingView?.center = CGPoint(
            x: touchLocation.x + touchOffsetFromCenterOfCell.x,
            y: touchLocation.y + touchOffsetFromCenterOfCell.y
          )
          
          self.dispatchOnMainQueueAfter(
            self.pingInterval,
            closure: {
              let shouldSwapCellsTuple = self.shouldSwapCells(touchLocation)
              if shouldSwapCellsTuple.shouldSwap {
                guard let newIndexPath = shouldSwapCellsTuple.newIndexPath else { return }
                self.swapDraggedCellWithCellAtIndexPath(newIndexPath)
              }
            }
          )
        case .ended:
          guard
            let draggedCellIndexPath = self.draggedCellIndexPath,
            case _ = self.draggingDelegate?.dragDropCollectionViewDraggingDidEndForCellAtIndexPath(self, indexPath: draggedCellIndexPath),
            let draggedCell = self.cellForItem(at: draggedCellIndexPath)
          else { return }
          UIView.animate(
            withDuration: 0.4,
            animations: {
              self.draggingView?.transform = .identity
              self.draggingView?.alpha = 1.0
              self.draggingView?.center = draggedCell.center
              self.draggingView?.layer.shadowRadius = 0
              self.draggingView?.layer.shadowColor = nil
              self.draggingView?.layer.shadowOffset = .zero
            },
            completion: { finished -> Void in
              self.draggingView?.removeFromSuperview()
              self.draggingView = nil
              draggedCell.isHidden = false
              self.draggedCellIndexPath = nil
            }
          )
        default:
          break
        }
      }
      
      func enableDragging(_ enable: Bool) {
        self.longPressRecognizer.isEnabled = enable
      }
      
      fileprivate func shouldSwapCells(_ previousTouchLocation: CGPoint) -> (shouldSwap: Bool, newIndexPath: IndexPath?) {
        guard
          self.isSwapEnabled,
          case let currentTouchLocation = self.longPressRecognizer.location(in: self),
          let draggedCellIndexPath = self.draggedCellIndexPath,
          Double(currentTouchLocation.x).isNaN.toggled,
          Double(currentTouchLocation.y).isNaN.toggled,
          self.distanceBetweenPoints(previousTouchLocation, secondPoint: currentTouchLocation) < CGFloat(20.0),
          let newIndexPathForCell = self.indexPathForItem(at: currentTouchLocation),
          self.cellForItem(at: draggedCellIndexPath) is DraggableCellType,
          self.cellForItem(at: newIndexPathForCell) is DraggableCellType,
          newIndexPathForCell != draggedCellIndexPath
        else { return (false, nil) }
        return (true, newIndexPathForCell)
      }
      
      fileprivate func swapDraggedCellWithCellAtIndexPath(_ newIndexPath: IndexPath) {
        guard let draggedCellIndexPath = self.draggedCellIndexPath else { return }
        let generator = UIImpactFeedbackGenerator(style: .light)
        generator.impactOccurred()
        self.moveItem(at: draggedCellIndexPath, to: newIndexPath)
        self.draggingDelegate?.dragDropCollectionViewDidMoveCellFromInitialIndexPath(
          self,
          initialIndexPath: draggedCellIndexPath,
          toNewIndexPath: newIndexPath
        )
        self.draggedCellIndexPath = newIndexPath
      }
    }
    
    //Assisting Functions
    extension DragDropCollectionView {
      func getRasterizedImageCopyOfCell(_ cell: UICollectionViewCell) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(cell.bounds.size, false, 0.0)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        cell.layer.render(in: context)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
      }
      
      func dispatchOnMainQueueAfter(_ delay: Double, closure: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(
          deadline: .now() + delay,
          qos: .userInteractive,
          flags: .enforceQoS,
          execute: closure
        )
      }
      
      func distanceBetweenPoints(_ firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
        let xDistance = firstPoint.x - secondPoint.x
        let yDistance = firstPoint.y - secondPoint.y
        return sqrt(xDistance * xDistance + yDistance * yDistance)
      }
    }


DragDropCollectionViewDelegate 사용 방법

  • CollectionView를 가지고 있는 MyView를 구현하고, ViewController에서 MyView를 사용하는 형식
  • ViewController 하는 일
    • myView.dragDropCollectionView의 데이터 소스 컨트롤
    • myView.dragDropCollectionView의 drag or drop 애니메이션 설정

  • MyView안에 DragDropCollectionView UI 코드
      // MyView.swift
      
      enum Metric {
        static let collectionViewPadding = 4.0
        static let collectionViewNumberOfColumns = 3.0
        static let collectionViewInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
        static let collectionViewItemSize: CGSize = {
          let collectionViewLeftRightInset = collectionViewInset.left + collectionViewInset.right
          let cellsWidth = UIScreen.main.bounds.width - collectionViewLeftRightInset - collectionViewPadding * (collectionViewNumberOfColumns - 1)
          let width = cellsWidth / collectionViewNumberOfColumns
          let height = width * 100 / 120
          return CGSize(width: width, height: height)
        }()
      }
    
      let dragDropCollectionView = DragDropCollectionView<MyCell>(
        frame: .zero,
        collectionViewLayout: UICollectionViewFlowLayout().then {
          $0.minimumLineSpacing = Metric.collectionViewPadding
          $0.minimumInteritemSpacing = Metric.collectionViewPadding
          $0.itemSize = Metric.collectionViewItemSize
        }
      ).then {
        $0.backgroundColor = .clear
        $0.enableDragging(true)
        $0.register(cellType: MyCell.self)
      }​
  • ViewController에서 위 collectionView 사용
    // ViewController.swift
    
    private let containerStackView = UIStackView().then {
      $0.axis = .vertical
    }
    private let addButton = UIButton().then {
      $0.setTitle("추가", for: .normal)
      $0.setTitleColor(.systemBlue, for: .normal)
      $0.setTitleColor(.blue, for: .highlighted)
    }
    private let myView = MyView()​
    
    override func viewDidLoad() {
      super.viewDidLoad()
      
      self.addButton.addTarget(self, action: #selector(addItem), for: .touchUpInside)
      
      self.view.addSubview(self.containerStackView)
      self.containerStackView.addArrangedSubview(self.addButton)
      self.containerStackView.addArrangedSubview(self.myView)
      self.containerStackView.snp.makeConstraints {
        $0.edges.equalTo(self.view.safeAreaLayoutGuide)
      }
    }
  • dataSource 처리
    // ViewController.swift
    
    // in viewDidLoad()
    self.myView.dragDropCollectionView.dataSource = self​
    
    ...
    
    extension ViewController: UICollectionViewDataSource {
      func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        self.colorDataSource.count
      }
      func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(for: indexPath, cellType: MyCell.self)
        cell.prepare(color: self.colorDataSource[indexPath.item])
        return cell
      }
    }
  • drag & drop 델리게이트 준수
    • 델리게이트 구현
      - didBegin
      - didMove
      - didEnd
      // ViewController.swift
      
      // in viewDidLoad()
      self.myView.dragDropCollectionView.draggingDelegate = self​
      
      ...
      
      extension ViewController: DrapDropCollectionViewDelegate {
        func dragDropCollectionViewDraggingDidBeginWithCellAtIndexPath<T>(_ collectionView: DragDropCollectionView<T>, indexPath: IndexPath) where T : UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(for: indexPath, cellType: MyCell.self)
          cell.alpha = 0.1
        }
        
        func dragDropCollectionViewDidMoveCellFromInitialIndexPath<T>(_ collectionView: DragDropCollectionView<T>, initialIndexPath: IndexPath, toNewIndexPath newIndexPath: IndexPath) where T : UICollectionViewCell {
          let item = self.colorDataSource[initialIndexPath.item]
          self.moveItem(from: initialIndexPath, to: newIndexPath, item: item)
        }
        
        func dragDropCollectionViewDraggingDidEndForCellAtIndexPath<T>(_ collectionView: DragDropCollectionView<T>, indexPath: IndexPath) where T : UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(for: indexPath, cellType: MyCell.self)
          cell.alpha = 1
        }
      }


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

 

* 참고

https://github.com/Lior539/DragDropCollectionView

Comments