본문 바로가기

IOS

Swift - Photos + CollectionView + Drag and Drops

 

https://moonggi-dev-story.tistory.com/46 해당 게시글에 이어서

 

이번엔 디바이스에 저장되어있는 이미지를 드래그 앤 드롭으로 CollectionView의 순서를 바꾸는 동작을 진행하자.

해당 기능을 사용하기 위해는 DataSoruce 를 사용해야 한다.

하지만 기존에 RxCocoa 상에서 그대로 DataSoruce를 사용하게 되면 오류를 발생한다.

그러기 때문에 DataSoruce를 따로 만들어서 사용한다.

 

1. DataSoruce

import UIKit
import RxCocoa
import RxSwift

class DragDataSoruce : NSObject, UICollectionViewDataSource, RxCollectionViewDataSourceType {
    
    typealias Element = [UIImage]
    var values = [UIImage]()

    func collectionView(_ collectionView: UICollectionView, observedEvent: Event<[UIImage]>) {
    
        if case .next(let element) = observedEvent {
            values = element
            collectionView.reloadData()
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let item = values.remove(at: sourceIndexPath.row)
        values.insert(item, at: destinationIndexPath.row)
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return values.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GalleryItem", for: indexPath) as! GalleryItem
        
        cell.imageview.image = values[indexPath.row]
        return cell
    }
}

2. ViewController

import UIKit
import RxCocoa
import RxSwift
import Photos

class DragViewContoller: UIViewController {
    
    let disposeBag = DisposeBag()
    let dragDataSoruce = DragDataSoruce()
    let listObservable = PublishSubject<[UIImage]>()
    @IBOutlet weak var mCollView: UICollectionView!
    
    var imageArray = Array<UIImage>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mCollView.register(UINib(nibName: "GalleryItem", bundle: nil), forCellWithReuseIdentifier: "GalleryItem")
        
        mCollView.allowsSelection = false
        mCollView.allowsMultipleSelection = false
        mCollView.rx.setDelegate(self).disposed(by: disposeBag)
        
        guard let layout = mCollView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        
        layout.itemSize = CGSize(width: ((self.view.bounds.width - 20) / 3), height: ((self.view.bounds.width - 20) / 3))

        let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:)))
        mCollView.addGestureRecognizer(gesture)
        
        initCollView()
        allGrabPhotos()
    }
    
    func initCollView() {
        
        listObservable.asObserver()
            .bind(to: mCollView.rx.items(dataSource: dragDataSoruce))
            .disposed(by: disposeBag)
    }
    
    func allGrabPhotos(){
        imageArray = []

        DispatchQueue.global(qos: .background).async {

            let imgManager=PHImageManager.default()

            let requestOptions=PHImageRequestOptions()
            requestOptions.isSynchronous=true
            requestOptions.deliveryMode = .highQualityFormat

            let fetchOptions=PHFetchOptions()
            fetchOptions.sortDescriptors=[NSSortDescriptor(key:"creationDate", ascending: false)]

            let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)

            if fetchResult.count > 0 {
                for i in 0..<fetchResult.count{
                    imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:50, height: 50),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
                        if let img = image
                        {
                            self.imageArray.append(img)
                        }
                    })
                }
            }

            self.listObservable.onNext(self.imageArray)
        }
    }
    
    @objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
       guard let collectionView = mCollView else {
           return
       }
       
       switch gesture.state {
       case .began:
           guard let targetIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) else {
               return
           }
           
           collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
       case .changed:
           collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: collectionView))
       case .ended:
           collectionView.endInteractiveMovement()
       default:
           collectionView.cancelInteractiveMovement()
       }
    }
}

뷰 컨트롤 상에서 롱클릭 제스처 줘서 해당 이벤트는 CollectionView DataSoruce에서 처리한다

해당 2가지 파일만 작성하면 간단하게 drag and drop 을 구현할 수가 있다.


3. 실행결과

 

 

뭔가 이상하쥬?

CollectionView 구현할때 셀 사이즈 고정을 위해 eslmate size를 none으로 설정해야 합니다.

 

 

편-안


대부분 갤러리 관련 앱을 만들 때 첫 번째 아이템 같은 경우는 default 이미지를 둬서 전화면이나 이미지를 다시 선택할 수 있게 프로세스를 두곤 합니다. 물론 첫 번째 아이템에서는 드래그 앤 드롭이 안 먹혀야 하고 이동도 불가능하게 만들어야겠죠?

extension DragViewContoller: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
        if proposedIndexPath.row == 0 {
            return IndexPath(row: 1, section: proposedIndexPath.section)
        } else {
            return proposedIndexPath
        }
    }
}

첫 번째 아이템이 아닌 두 번째 아이템 이후 아이템을 꾹 눌렀을 때 첫 번째 아이템 쪽으로 침범해도 움직여지지 않게 이벤트를 주는 로직입니다.

 

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
	let cell = collectionView.dequeueReusableCell(withReuseIdentifier: (indexPath.row == 0) ? "첫번째아이템" : "나머지아이템", for: indexPath)

  if indexPath.row == 0 {
    let castingCell = cell as! "첫번째아이템"
    
    castingCell.mContentView.rx.tapGesture()
    .when(.recognized)
    .subscribe(onNext : {
    [self] _ in
      //첫번째 아이템 클릭 이벤트
    }).disposed(by: castingCell.cellDisposedBag)

	return castingCell
  } else {
    let castingCell = cell as! "나머지아이템"

    castingCell.mImageView.image = values[indexPath.row].image

    return castingCell
  }
}

func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
  if indexPath.row == 0 {
  	return false
  } else {
  	return true
  }
}

편의상 첫번째 아이템과 나머지 아이템을 분리시켰습니다. 분리 후 클릭 이벤트 및 이미지 설정 가능합니다. canMoveItemAt 함수에서는 드래그 활성화 여부 체크합니다.

 

참고 :

  https://stackoverflow.com/questions/47528344/uicollectionview-how-to-prevent-one-cell-from-being-moved

 

UICollectionView, how to prevent one cell from being moved

I recently study collection view. I need to let some cells fix in their own index path, which mean they should not be exchanged by others and not be dragged. I now can use *- (BOOL)collectionView:(

stackoverflow.com

https://github.com/ReactiveX/RxSwift/issues/1081

 

UICollectionViewCell gets draggable when elements are binded via rx.items(dataSource:) · Issue #1081 · ReactiveX/RxSwift

Short description of the issue: UICollectionViewCell gets draggable when elements are binded via rx.items(dataSource:), even if the delegate does not have implementation of collectionView(_:canMove...

github.com

https://www.youtube.com/watch?v=VrW_6EixIVQ 

 

'IOS' 카테고리의 다른 글

Swift - ReactorKit2  (0) 2021.07.06
Swift - ReactorKit  (0) 2021.06.09
Swift - Photos + CollectionView + Gesture Multiple Select  (0) 2021.03.07
프로젝트에 SwiftLint를 달아보자  (0) 2021.03.01
SwiftUI - ViewPager 관련 기능을 달아보자  (0) 2021.02.16