Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- rxswift
- uitableview
- 리펙토링
- UITextView
- tableView
- 스위프트
- ribs
- HIG
- 리팩토링
- 애니메이션
- Protocol
- map
- Human interface guide
- combine
- Clean Code
- uiscrollview
- swift documentation
- MVVM
- clean architecture
- 클린 코드
- RxCocoa
- UICollectionView
- swiftUI
- SWIFT
- Observable
- collectionview
- ios
- 리펙터링
- Xcode
- Refactoring
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] DragDropCollectionView 구현 방법 (dragDelegate, dropDelegate, 드래그앤 드랍) 본문
UI 컴포넌트 (swift)
[iOS - swift] DragDropCollectionView 구현 방법 (dragDelegate, dropDelegate, 드래그앤 드랍)
jake-kim 2023. 7. 4. 01:49
주의사항
- 애플에서 제공하는 기본 Drag, Drop을 사용하면, UITextField에서 becomeFirstResponder 상태에서 resignFirstResponder로 변경되므로 셀을 이동시킬때 키보드가 내려가므로 주의
- 키보드가 내려가지 않게 하려면 Drag, Drop을 제공하는 커스텀 CollectionView 구현이 필요
- 키보드가 내려가지 않게 하려면 UILongPressGestureRecognizer를 사용하여 직접 커스텀 필요한데, 이 부분은 다음 포스팅 글참고
ex) 애플에서 기본으로 제공하는 drag, drop을 사용한 경우, 셀 drag를 하는 순간 키보드가 내려가는 문제
DragDropCollectionView 구현 아이디어
- collectionView의 drag, drop 델릴게이트를 준수
- UICollectionViewDragDelegate와 UICollectionViewDropDelegate에서 제공하는 메소드에서 drag, drop 이벤트 캐치
- collectionView의 dataSource 수정
- 해당 셀 업데이트
drag drop 구현 전, 수평 스크롤이 있는 UICollectionView 준비
- UICollectionViewFlowLayout의 scrollDirection을 .horizontal로 설정하고, UICollectionView의 heightAnchor와 itemSize의 height값을 동일하게 준것
import UIKit
final class MyCell: UICollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
prepare(color: nil)
}
func prepare(color: UIColor?) {
contentView.backgroundColor = color
}
}
class ViewController: UIViewController {
private enum Metric {
static let cellWidth = 80.0
static let cellHeight = 120.0
static let horizontalInset = 20.0
}
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let mainSize = UIScreen.main.bounds
layout.itemSize = .init(width: Metric.cellWidth, height: Metric.cellHeight)
layout.scrollDirection = .horizontal
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.contentInset = .init(top: 0, left: Metric.horizontalInset, bottom: 0, right: Metric.horizontalInset)
view.register(MyCell.self, forCellWithReuseIdentifier: "cell")
view.dataSource = self
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private var dataSource = (0...10).map { _ in UIColor.randomColor }
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
collectionView.heightAnchor.constraint(equalToConstant: Metric.cellHeight)
])
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
true
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? MyCell
else { return UICollectionViewCell() }
cell.prepare(color: dataSource[indexPath.row])
return cell
}
}
drag, drop 기능 구현
- dragDelegate, dropDelegate 준수 및 drag 활성화
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
...
view.dragDelegate = self
view.dropDelegate = self
view.dragInteractionEnabled = true
...
- UICollectionViewDragDelegate, UICollectionViewDropDelegate 준수
extension ViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
print("drag>", indexPath)
return []
}
}
extension ViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
print("drop>", coordinator.destinationIndexPath)
}
}
- itemsForBeginning의 구현은 단순히 아래처럼 반환해주면 완료
- NSItemProvider 개념은 이 포스팅 글 참고
extension ViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
[UIDragItem(itemProvider: NSItemProvider())]
}
}
- 핵심은 DropDelegate 구현부
UICollectionViewDropDelegate 구현
- drag하는 동안 계속 호출되는 메소드 dropSessionDidUpdate 구현
- drag가 활성화 되어 있는경우에만 drop이 동작하도록 구현
- drag없이 drop이 동작할 수 없도록 하는 메소드
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
guard collectionView.hasActiveDrag else {
return UICollectionViewDropProposal(operation: .forbidden)
}
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
- performDropWith 구현
- coordinator 프로퍼티에서 destinationIndexPath를 얻어낼 수 있는데, 만약 이 값이 없다면 destinationPath가 마지막 indexPath임을 이용
- coordinator를 사용하면 또 sourceIndexPath를 얻어낼 수 있는데 이를 이용하여 move를 구현
- coordinator 프로퍼티에서 destinationIndexPath를 얻어낼 수 있는데, 만약 이 값이 없다면 destinationPath가 마지막 indexPath임을 이용
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
var destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationIndexPath = IndexPath(item: row - 1, section: 0)
}
guard coordinator.proposal.operation == .move else { return }
move(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
private func move(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
// TODO
}
- coordinator로부터 sourceIndexPath를 얻어내고 performBatchUpdates 블락 안에서 dataSource와 UI 업데이트
- performBatchUpdates가 끝나면 cooridnator.drop()을 사용하여 drop 애니메이션 효과 주기
private func move(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
guard
let sourceItem = coordinator.items.first,
let sourceIndexPath = sourceItem.sourceIndexPath
else { return }
collectionView.performBatchUpdates { [weak self] in
self?.move(sourceIndexPath: sourceIndexPath, destinationIndexPath: destinationIndexPath)
} completion: { finish in
print("finish:", finish)
coordinator.drop(sourceItem.dragItem, toItemAt: destinationIndexPath)
}
}
private func move(sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) {
// TODO
}
- 주의) 애니메이션 효과를 주는 cooridnator.drop() 호출은 반드시 performBatchUpdates 이후에 수행해야함
- performBatchUpdates 안에서 호출되는 코드
- dataSource 업데이트
- UI 업데이트
private func move(sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) {
let sourceItem = dataSource[sourceIndexPath.item]
// dataSource 이동
dataSource.remove(at: sourceIndexPath.item)
dataSource.insert(sourceItem, at: destinationIndexPath.item)
// UI에 반영
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}
(완료)
(move했을때 dataSource가 변경되지 않은 케이스가 있으므로 정확히 하려면 아래처럼 move 메소드를 구현)
private func move(sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) {
let sourceItem = dataSource[sourceIndexPath.item]
// dataSource 이동
DispatchQueue.main.async {
self.dataSource.remove(at: sourceIndexPath.item)
self.dataSource.insert(sourceItem, at: destinationIndexPath.item)
let indexPaths = self.dataSource
.enumerated()
.map(\.offset)
.map { IndexPath(row: $0, section: 0) }
UIView.performWithoutAnimation {
self.collectionView.reloadItems(at: indexPaths)
}
}
}
cf) 원하는 뷰 영역만 드래그 할 때 보이도록 하는 방법은 다음 포스팅 글에서 계속
* 전체 코드: https://github.com/JK0369/DragDropCollectionView_Example
* 참고
https://developer.apple.com/documentation/uikit/uicollectionviewdropdelegate/2897375-collectionview
https://developer.apple.com/documentation/uikit/uicollectionviewdragdelegate
https://developer.apple.com/documentation/uikit/uicollectionviewdropdelegate
https://developer.apple.com/documentation/uikit/uicollectionviewdropdelegate/2897304-collectionview
'UI 컴포넌트 (swift)' 카테고리의 다른 글
[iOS - swift] 2. 키보드 처리 - 키보드가 올라갈 때 스크롤 뷰를 올리는 UI (0) | 2023.07.11 |
---|---|
[iOS - swift] 1. 키보드 처리 - 키보드가 올라갈 때 뷰를 올리는 UI (0) | 2023.07.10 |
[iOS - swift] 3. 갤러리 화면 만들기, 사진 첨부 - 갤러리 화면 UI 구현 방법 (0) | 2023.06.29 |
[iOS - swift] 2. 갤러리 화면 만들기, 사진 첨부 - 사진 가져오기 (PHCachingImageManager, PHImageRequestOptions) (0) | 2023.06.28 |
[iOS - swift] 1. 갤러리 화면 만들기, 사진 첨부 - 앨범 가져오기 (PHFetchResult, PHAsset) (3) | 2023.06.27 |
Comments