본문 바로가기

개발/Swift

[Swift] 테이블 뷰 -> 테이블 뷰 drag and drop

반응형

앱을 디자인해보다 처음 해보게 된 TableView1 -> TableView2 드래그 앤 드롭
테이블뷰1 에서 테이블뷰2로 옮겨보는 것이 가능한가..?

결론부터 말하자면 가능하다!
애플 문서 왈 드래그 앤 드롭 작업은 기종에 따라 가능한 역할이 나뉘는데
iPhone에서는 하나의 앱에서 수행되는 것이 가능하고
iPad에서는 위 기능에 더해 한 앱에서 다른 앱으로 이동할 수 있다고 한다 

드래그 앤 드랍을 테이블 뷰에서 만들어보자
테이블 뷰에 기본적인 요구조건을 넣어준다

class TestViewContoller : UITableViewDelegate, UITableViewDataSource { 
    tableView.delegate = self
    tableView.dataSource = self
    tableView.dragInteractionEnabled = true
    ... 
}

 

드래그 delegate 를 먼저 살펴보자 

UITableViewDragDelegate를 따르게 되면 아래 함수가 필수적으로 나타나는데
이는 드래그 되는 아이템의 배열을 요구한다

extension TestViewController : UITableViewDragDelegate {
    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        return [UIDragItem(itemProvider: NSItemProvider())]
    }
}

 

그렇다면 여기에 내 모델을 넣어주고 드래그 하면 되겠군! 

반은 맞고 반은 틀리다...
UIDragItem 타입은 NSItemProviderWriting & Reading 프로토콜을 충족시키는 데이터만 사용이 가능하다

바로 사용 가능한 데이터는 NSStirng, UIImage, NSTextStorage 등 소수의 데이터 유형만 채택되기에 
우리가 만든 모델을 넣고 싶다면 위 프로토콜을 모델에 붙여 요구사항을 충족해야한다

 

Model (UIDragItem)

final class Subject : NSObject{
    let id : String
    
    override init() {
        id = ""
        super.init()
    }
    
    init(id : String) {
        self.id = id
    }
}

 

위의 모델을 사용할건데 drag 할 수 있도록 요구사항을 넣어보자
인자가 String 하나인데 class로 만든 이유는 자체 모델을 적용해보기 위함이다!

import MobileCoreServices

extension Subject : NSItemProviderWriting {
    static var writableTypeItentifiersForItemProvider : [String] {
        return [String(kUTTypeData)]
    }
    
    func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        let progress = Progress(totalUnitCount: 100)
        do {
            let data = try JSONEncoder().encode(self)
            progress.completedUnitCount = 100
            completionHandler(data, nil)
        } catch {
            completionHandler(nil, error)
        }
        return progress
    }
}

 

kUTTypeData는 String의 유니폼 타입으로 ImageCaptureCore - ICCameraItem - uti 소속이다
간단하게 설명하면 데이터에 대한 유형 정보를 담고 있다 
import MobileCoreServices해서 이걸 사용해주자

londData 블라블라 함수는 클래스의 데이터 형식을 지정된 형식으로 변환하는 함수다
데이터 변환 = JSONEndocer
잠깐 데이터를 변환한다? Codable!
NSItemProviderReading과 함께 Codable을 사용한다

extension Subject : Codable, NSItemProviderReading {
    static var readableTypeIdentifiersForItemProvider: [String] {
        return [String(kUTTypeData)]
    }
    
    static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Subject {
        do {
            let subject = try JSONDecoder().decode(Subject.self, from: data)
            return subject
        } catch {
            fatalError()
        }
    }
}

 

모델 인코딩 디코딩 작업 끝!
UIDragItem의 데이터 형식에 맞게 커스텀했으니, 이제 drag를 다루어보자

 

 

+TableViewSetting 

기본적인 테이블 뷰 세팅 혹시나 해서 추가했다
클래스 상단에 각 테이블에서 사용하는 모델과 selectedIndexPath 프로퍼티를 만들어두었다

class TestViewController {
    // 테이블 뷰 2개지만 예시니까 하나만 작성합니다
    tableView.dropDelegate = self
    tableView.dragDelegate = self
    ...
    private var selectIndexPath : (IndexPath, Bool)?
    private var firstItems = [Subject(id: "네이버"), Subject(id: "카카오"), Subject(id: "라인")]
    private var secondItems = [Subject(id: "할리스"), Subject(id: "스타벅스"), Subject(id: "투썸")]
    ...
}

 

selectIndexPath는 드래그 후 드롭하기 직전 해당 인덱스를 기억하기 위해 만들었고 
아래 items는 각 테이블에서 사용하는 배열이다

extension TestViewController : UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableView == firstTableView ? firstItems.count : secondItems.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: projectBoardTableViewCell.cellID) as! projectBoardTableViewCell
        cell.todoLabel.text = tableView == firstTableView ?
            firstItems[indexPath.row].id : secondItems[indexPath.row].id
        return cell
    }
}

 

 

Drag Session

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    let subject = subjects[indexPath.row]
    let itemProvider = NSItemProvider(object: subject)
    
    // 테이블 뷰에 따른 모델 & 나중에 쓸 것!
    let model = tableView == self.firstTableView ? self.firstItems : self.secondItems
    selectIndexPath = (indexPath, false)
    
    return [UIDragItem(itemProvider: itemProvider)]
}

 

자! 이제 우리가 만든 Subject 모델을 드래그 할 수 있다
드래그 후 드랍을 하기 위해 UITableViewDropDelegate를 추가해준다

func tableView(_ tableView: UITableView, dragSessionDidEnd session: UIDragSession) {
    guard let selectIndexPath = selectIndexPath else { return }
    if selectIndexPath.1 {
        if tableView == firstTableView {
            firstItems.remove(at: selectIndexPath.0.row)
        } else {
            secondItems.remove(at: selectIndexPath.0.row)
        }
        tableView.beginUpdates()
        tableView.deleteRows(at: [selectIndexPath.0], with: .automatic)
        tableView.endUpdates()
    }
}

 

드래그가 끝났을 경우,  드롭이 이루어지기 직전을 컨트롤 할 수 있는 곳이다
테이블 뷰에서 내보내는 정보의 변화가 생겼을 경우 (cell 개수 변동 등)
tableView.beginUpdates() 와 tableView.endUpdates()를 사용해서
테이블에게 정보가 바뀌었음을 알려주어야한다 

 

Drop Session

우리는 커스텀 데이터를 사용했으니 (Subject) 아래 함수로 사용하는 모델을 알려주어야한다

extension TestViewController : UITableViewDropDelegate {
    func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
        return session.canLoadObjects(ofClass: Subject.self)
    }
}

 

드롭 세션에 변화가 생겼을 때, 아래 함수를 호출한다
드래그가 같은 테이블에서 일어나는지, 다른 테이블에서 일어나는지 (outside from app) 정보를 알 수 있다
이걸로 셀을 복사할지, 움직일지 결정할 수 있다
selectIndexPath의 두번째를 Bool로 설정했는데 이유는 같은 테이블 뷰인지 확인하기 위함이다
(같은 테이블의 경우 셀을 삭제할 필요가 없기 때문에)

func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
        var dropProposal = UITableViewDropProposal(operation: .cancel)
        guard session.items.count == 1 else { return dropProposal }
       
        if tableView.hasActiveDrag {
            if tableView.isEditing {
                dropProposal = UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
            }
        } else {
            if let indexPath = selectIndexPath {
                selectIndexPath = (indexPath.0, true)
            }
            // drag is comming from outside from app
            return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
        }
        return dropProposal
    }

 

드롭이 일어났을 경우 테이블에도 변화가 생긴다!
테이블 배열에 정보를 넣어주며 동시에 테이블에 새로운 row를 넣어준다

func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
    var destinationIndexPath = IndexPath(row: tableView.numberOfRows(inSection: 0), section: 0)
    if let indexpath = coordinator.destinationIndexPath {
        destinationIndexPath = indexpath
    }
    
    coordinator.session.loadObjects(ofClass: Subject.self) { [self] items in
        guard let subject = items as? [Subject] else { return }
        var indexPaths = [IndexPath]()
        
        for (index, value) in subject.enumerated() {
            let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
            
            if tableView == self.firstTableView {
                self.firstItems.insert(value, at: indexPath.row)
            } else {
                self.secondItems.insert(value, at: indexPath.row)
            }
            indexPaths.append(indexPath)
        }
        
        tableView.beginUpdates()
        tableView.insertRows(at: indexPaths, with: .automatic)
        tableView.endUpdates()
    }
}

 

이렇게 실행하면..! 매우 잘된다!!

 

 

출처 & 참고자료

 

Apple Developer Documentation

 

developer.apple.com

 

Drag and drop with custom classes using Codable in iOS 11

Introduction

medium.com

반응형

'개발 > Swift' 카테고리의 다른 글

[Swift] 프로토콜 지향 제네릭  (0) 2021.06.03
[Swift] TextField 앞 뒤 공백 없애기  (0) 2021.04.22
[Swift] 문자열 다루기  (0) 2021.03.31
[Swift] Core Data 알아보기 (2)  (0) 2021.03.02
[Swift] Core Data 알아보기 (1)  (0) 2021.03.01