Swift 정리 - UITableView Drag and Drop

김세영·2022년 6월 3일
0

Doc - Article 정리

목록 보기
4/4
post-thumbnail

출처: Adopting Drag and Drop in a Table View

위 문서의 샘플 코드를 정리한 내용입니다.

테이블 뷰에서 드래그 앤 드롭이 가능하게 하는 방법을 알아봅니다.
이를 위해서는 DragDelegate, DropDelegate를 채택하여야 하고, 추가적인 기능을 위해서는 델리게이트 메서드를 구현해야 합니다.

이 앱은 외부에서 드래그 된 텍스트를 추가할 수 있고, Row를 드래그하여 Row의 순서를 바꿀 수도 있습니다.
드래그 앤 드롭 API를 사용하지 않고, tableView(_:canMoveRowAt:)tableView(_:moveRowAt:to:) 메서드를 사용합니다.

Enable Drag and Drop Interactions

드래그 및 드롭을 하기 위해서는 각각의 Delegate를 채택 및 구현해야 합니다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    tableView.dragInteractionEnabled = true
    tableView.dragDelegate = self
    tableView.dropDelegate = self

    navigationItem.rightBarButtonItem = editButtonItem
}

커스텀 뷰와 다르게 TableView는 interactions 프로퍼티가 존재하지 않아, 따로 상호작용을 추가할 수 없습니다.
대신 해당 델리게이트들을 사용합니다.

Provide Data for a Drag Session

TableView에서 드래깅이 시작될 때 데이터를 제공할 때는
tableView(_:itemsForBeginning:at:) -> [UIDragItem] 메서드를 구현하여야 합니다.

아이폰을 사용하다 보면 어떠한 앱에서 드래그를 한 후,
다른 앱에 드롭하여 해당 컨텐츠를 추가할 수 있는 경우가 있습니다.
이는 NSItemProvider을 통해 구현할 수 있습니다.

이 앱에서는 드래그를 시작할 때 NSItemProvider을 사용하여 해당 기능을 구현하였습니다.

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    return model.dragItems(for: indexPath)
}

func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
    let placeName = placeNames[indexPath.row]

    let data = placeName.data(using: .utf8)
    let itemProvider = NSItemProvider()
    
    itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in
        completion(data, nil)
        return nil
    }

    return [
        UIDragItem(itemProvider: itemProvider)
    ]
}

Comsume Data from a Drop Session

드롭 세션을 사용하기 위해서는 세 가지의 델리게이트 메서드를 구현해야 합니다.

tableView(_:canHandle:) -> Bool

앱은 상태에 따라, 또는 특정 요구 사항 등에 따라 드롭을 거절해야 하는 상황이 발생할 수 있습니다.

해당 앱은 데이터가 NSString의 인스턴스일 때만 드롭할 수 있도록 해 놓은 모습입니다.

func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
    return model.canHandle(session)
}

func canHandle(_ session: UIDropSession) -> Bool {
    return session.canLoadObjects(ofClass: NSString.self)
}

tableView(_:dropSessionDidUpdate:withDestinationIndexPath:) -> UITableViewDropProposal

개발자는 시스템에게 데이터를 어떻게 제공해야 할 지(일반적으로 복사 등)를 알려주어야 합니다.
이는 해당 메서드에서 Drop Proposal을 반환하여 알려줄 수 있습니다.

아래 코드에서는 다음 조건으로 분기하여 Drop Proposal을 결정합니다.

  • 아이템의 개수가 1개가 아니면 cancel 반환
    • 한 개의 드래그 아이템만을 관리
  • 테이블 뷰에서 활성화 된 드래그가 있고, 테이블 뷰가 editing 상태라면 move 반환
    • 앱 내부에서 드래그가 발생된 경우
  • 테이블 뷰에 활성화 된 드래그가 없다면 copy 반환
    • 앱 외부에서 드래그가 들어온 경우
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(_:performDropWith:)

사용자가 드롭을 완료할 때 (드래그 후 스크린에서 손가락을 뗄 때), 테이블 뷰는 드래그 아이템에 대한 데이터를 요청할 수 있습니다.

이 앱에서는 해당 메서드에서 아이템이 삽입될 위치와,
삽입 시 처리해야 할 DataSource의 배열 아이템 수정 및
TableView의 indexPath 수정 등을 진행합니다.

func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
    let destinationIndexPath: IndexPath
    
    if let indexPath = coordinator.destinationIndexPath {
        destinationIndexPath = indexPath
    } else {
        // Get last index path of table view.
        let section = tableView.numberOfSections - 1
        let row = tableView.numberOfRows(inSection: section)
        destinationIndexPath = IndexPath(row: row, section: section)
    }
    
    coordinator.session.loadObjects(ofClass: NSString.self) { items in
        // Consume drag items.
        let stringItems = items as! [String]
        
        var indexPaths = [IndexPath]()
        for (index, item) in stringItems.enumerated() {
            let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
            self.model.addItem(item, at: indexPath.row)
            indexPaths.append(indexPath)
        }

        tableView.insertRows(at: indexPaths, with: .automatic)
    }
}

정리

  • Drag
    • tableView(_:itemsForBeginning:at:) -> [UIDragItem] 에서 드래그가 시작되고, 드래그 할 아이템을 반환
  • Drop
    • tableView(_:canHandle:) -> Bool 메서드에서 앱이 해당 아이템을 드롭할 수 있는지 판단
    • tableView(_:dropSessionDidUpdate:withDestinationIndexPath:) -> UITableViewDropProposal 에서 앱이 드롭될 데이터를 어떻게 처리해야 할 지 결정
    • tableView(_:performDropWith:)에서 드롭이 완료되었을 때 헤당 데이터를 가지고 특정 행동을 실행
profile
초보 iOS 개발자입니다ㅏ

0개의 댓글