Drag & Drop with UITableView

Overview

Trên iPad với iOS 11, chúng ta có thể thực hiện truyền dữ liệu giữa các app bằng thao tác kéo thả một đối tượng như text, image bằng cách adopt các phương thức hỗ trợ việc kéo thả nằm trong delegate. Cùng xem các thực hiện như thế nào nhé:

Get Started

Việc cần làm đầu tiên là tạo một project chạy trên iPad, thiết bị hỗ trợ drag và drop giữa các app. Chúng ta sẽ tìm cách đển có thể gửi và nhận text từ một app khác cũng hỗ trợ drag và drop ví dụ như app mặc định Reminders. Bật chế độ Split View trên iPad, một bên là app của chúng ta, một bên là app Reminders. Và bây giờ hãy xem làm thế nào để enable hai tính năng drag và drop

Enable Drag and Drop Interactions

Để có thể implement các phương thức cho phép drag hoặc drop (hoặc cả hai), bước đầu tiên chúng ta cần cài đặt delegate cho TableView trong ViewDidLoad: Đoạn code sau sẽ enable cả hai chức năng drag và drop:

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

Không giống như một custom View, đối tượng UITableView không có thuộc tính interactions để chúng ta gán các tương tác, thay vào đó nó dùng trực tiếp các phương thức drag và drop thông qua delegate.

Cung cấp dữ liệu cho một Drag Session

Để đưa dữ liệu vào một thao tác drag từ UITableView chúng ta implement phương thức tableView(_:itemsForBeginning:at:)

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

Phương thức dragItems được định nghĩa sau đây cho phép chuyển từ giao diện người dùng thành dạng dữ liệu như sau:

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)
    ]
}

Ở các bước sau, chúng ta sẽ xem làm thế nào để xử lý dữ liệu nhận được từ thao tác drop.

Nhận dữ liệu từ một Drop Session

Để nhận dữ liệu từ một drag session từ UITableView, chúng ta cần implement ba phương thức delegate.
Đầu tiền, ứng dụng của bạn có thể lựa chọn hoặc từ chối một số dữ liệu trong drop session tùy vào kiểu của dữ liệu đó; hay đơn giản hơn là tùy vào trạng thái hiện tại ứng dụng của bạn hoặc bất kỳ một yêu cầu nào khác. Trong đoạn code mẫu dưới đây, người viết giả sử ứng dụng này chỉ tiếp nhận các đối tượng có kiểu là NSString.

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

Trong phương thức canHandle() được gọi đến bên trên, chúng ta tiến hành đưa đối tượng UIDropSession về dạng dữ liệu kiểu String:

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

Tiếp theo, việc cần làm là cho hệ thống biết các mà chúng ta sẽ xử lý các dữ liệu nhận được trong drop session, thường là copy chúng.

func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
    // The .move operation is available only for dragging within a single app.
    if tableView.hasActiveDrag {
        if session.items.count > 1 {
            return UITableViewDropProposal(operation: .cancel)
        } else {
            return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        }
    } else {
        return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
    }
}

Cuối cùng, sau khi người dùng nhấc ngón tay khỏi màn hình, phương thức tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) sẽ được gọi đến, và chúng ta tiến hành xử lý các dữ liệu nhận được và cập nhật chúng lên TableView

/**
     This delegate method is the only opportunity for accessing and loading
     the data representations offered in the drag item. The drop coordinator
     supports accessing the dropped items, updating the table view, and specifying
     optional animations. Local drags with one item go through the existing
     `tableView(_:moveRowAt:to:)` method on the data source.
*/
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)
    }
}

Ngoài ba phương thức trên, iOS 11 cũng hỗ trợ thêm một số phương thức khác để chúng ta có thể customize cách mà drag và drop hoạt trộng trên TableView của mình, độc giả có thể tìm hiểu thêm tại đây: Supporting Drag and Drop in Table Views

Trong bài viết tiếp theo của seri này, tôi sẽ đề cập đến drag & drop trên một custom view, hi vọng nhận được sự quan tâm từ các bạn. Thank for reading.