앱을 디자인해보다 처음 해보게 된 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()
}
}
이렇게 실행하면..! 매우 잘된다!!
출처 & 참고자료
'개발 > 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 |