[iOS] UITableView drag and drop

Charlie·2022년 11월 10일
0

프로젝트 진행 도중에 TableView의 cell을 꾹 눌러서 cell의 위치를 바꾸고 datasource도 순서를 바꾸게 하는 작업을 하게 되었어요.
앱을 사용하면서 많이 사용해본 경험이 있지만 구현은 한번도 해보지 않아서 조금 쫄았지만.. 찾아보니 아주 쉽게 할 수 있더라고요!😅
그래서 오늘은 UITableView의 cell을 드래그를 통해 움직이는 방법에 대해 알아보려고 해요.


DataSource는 무엇으로?

우선 cell의 위치만 변경시키는 것이 아닌, data source도 수정을 해야해서 막 찾아보기 시작했을 때에는 Diffable Data Source를 사용하지 못하나?? 라는 의문을 가졌지만
일반적으로 data source를 사용할 때에도, diffable datasource를 사용할 때에도 모두 가능해서 다행이라고 생각했어요


DragDelegate, DropDelegate

우선 drag & drop 기능을 사용하기 위해서는 tableView에서 먼저 설정을 해줘야 하는 것들이 있어요

  • dragInteractionEnabled : 테이블 뷰의 드래그를 활성화합니다
  • dragDelegate : drag 제스쳐의 delegate을 설정합니다
  • dropDelegate : drop 제스쳐의 delegate을 설정합니다

VC에서 다음과 같이 사용하는 테이블 뷰를 설정해줍니다

tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self

그리고 dragDelegate, dropDelegateself로 설정했으니 각 프로토콜을 채택해야겠죠?
채택 후 필수적으로 구현해야 하는 메소드는 아래 코드 가운데 위에 2개입니다.

그렇지만 drop을 처리하기 위해서 dropSessionDidUpdate 메소드를 추가로 구현합시다

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
	return [UIDragItem(itemProvider: NSItemProvider())]
}
    
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
    // 빈 block으로 둡니다
}

func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
    if session.localDragSession != nil {
        return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
    }
    return UITableViewDropProposal(operation: .cancel, intent: .unspecified)
}

공식문서에서는 아래와 같이 dropSessionDidUpdate 메소드의 예시를 보여주고 있어요

func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
    var dropProposal = UITableViewDropProposal(operation: .cancel)
    
    // Accept only one drag item.
    guard session.items.count == 1 else { return dropProposal }
    
    // The .move drag operation is available only for dragging within this app and while in edit mode.
    if tableView.hasActiveDrag {
        if tableView.isEditing {
            dropProposal = UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        }
    } else {
        // Drag is coming from outside the app.
        dropProposal = UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
    }

    return dropProposal
}

주석을 읽어보면 알 수 있듯이, 하나의 아이템만 드래그 가능하게 설정하는 코드, tableView가 edit 일 때 move 드래그, 이 외에는 다른 앱에서의 드래그임을 알 수 있네요

프로젝트에 맞게 잘 사용하면 좋을 것 같아요


자! 이까지 했으면 tableView가 움직이긴 합니다!!

근데 다시 원래 자리로 돌아가요..ㅠㅠㅠ😢

drop은 했지만 data source는 바뀌지 않아서 다시 원래대로 돌아가는거죠!


DataSource도 바꿔주기

자... 요놈들을 다시 되돌아가지 않도록 해봅시다!

tableView(_: moveRowAt: to:)

요 메소드를 사용해서 data source를 바꿔줄겁니다!!
지금은 ViewModel을 사용하고 있기 때문에... 해당 메소드에서 viewModel의 메소드를 호출해줍시다

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    viewModel.changePlaceOrder(sourceIndex: sourceIndexPath.row, to: destinationIndexPath.row)
}

ViewModel에게 '여기랑 저기랑 위치를 바꿔줘!' 라고 말해주고,
ViewModel에서는 아래와 같이 메소드를 구현했어요

func changePlaceOrder(sourceIndex: Int, to destinationIndex: Int) {
	let targetPlace = places[sourceIndex]
	places.remove(at: sourceIndex)
    places.insert(targetPlace, at: destinationIndex)
}

그럼 이제 예전처럼 다시 돌아가지 않고 제대로 작동을 하게 됩니당 🎉


UIDragPreview

현재 프로젝트에서는 굳이 필요없지만 나중에 필요할 수 있어서 UIDragPreview를 통해 꾸-욱 눌렀을 때 cell의 UI를 설정하는 방법에 대해 설명해주신 Phililip님의 블로그를 남겨놓을게요 😁


Reference

Apple Developer Documentation - Adopting Drag and Drop in a Table View
[iOS] UITableView Drag & Drop으로 Row 이동
DiffableDataSource를 사용해서 TableView 드래그&드롭 기능 넣기

profile
Hello

0개의 댓글