iOS 11: Drag & Drop with Collection View (part 3)

Drag & Drop with Collection View

Overview

Ở bài viết trước, tôi đã đề cập đến việc adopt tính năng drag và drop trên đối tượng Custom View, các bạn có thể tham khảo tạo đây: iOS 11: Drag & Drop with Custom View (part 2). Trong khuôn khổ của bài viết này tôi xin đề cập đến cách mà chúng ta có thể enable tính năng drag và drop trên đối tượng UICollectionView.

Getting Started

Để hỗ trợ tính năng drag trên collection view , chúng ta cần xây dựng một object mà trong đó implement các giao thức của UICollectionViewDragDelegate và sau đó gán nó vào thuộc tính dragDelegate của collection view. Để hỗ trợ tính năng drop, chúng ta xây dựng một object mà trong đó implement các giao thức của UICollectionViewDropDelegate và sau đó gán nó vào thuộc tính dropDelegate của collection view.

Enable tính năng Drag và Drop

Để 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 collection view trong ViewDidLoad: Đoạn code sau sẽ enable cả hai chức năng drag và drop:

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

Không giống như một custom View, đối tượng UICollectionView không có thuộc tính interactions giống như một Custom view để 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

UICollectionView đã tự động quản lý hầu hết tất cả các tương tác liên quan đến hành động drag, nhưng chúng ta cần chỉ định đích danh các item cần drag. Khi thao tác drag được thực hiện, UICollectionView tạo ra một drag session và gọi đến phương thức collectionView(_:itemsForBeginning:at:) của dragDelegate. Nếu bạn trả lại một mảng không rỗng từ phương thức này đó, collection view sẽ bắt đầu cho phép drag các item mà bạn chỉ định. Trả lại một mảng rỗng khi bạn không cho phép người dùng kéo các item từ đường dẫn chỉ mục được chỉ định.

Sử dụng các phương thức khác của UICollectionViewDragDelegate để quản lý các tương tác khác liên quan đến hành động drag. Ví dụ: bạn có thể tùy chỉnh giao diện của các item đang được drag và cho phép người dùng thêm các item vào drag session hiện tại.

Trong phần implement phương thức collectionView (: itemsForBeginning: at 😃 của bạn, hãy thực hiện theo các bước sau:

  1. Tạo một hoặc nhiều đối tượng NSItemProvider. Sử dụng instance có kiểu NSItemProvider này để đại diện cho dữ liệu được drag trong collectionView.

  2. Tạo một đối tượng có kiểu UIDragItem được tạo bởi NSItemProvider ở bước trước.

  3. Xem xét việc gán một giá trị cho thuộc tính localObject của mỗi drag item. Chúng ta có thể thực hiện bước này hoặc không, nhưng nó sẽ làm cho thao tác drag và drop nhanh hơn trong cùng một ứng dụng.

  4. Return đối tượng UIDragItem vừa tạo.

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

func collectionView(_ collectionView: UICollectionView, 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)
    ]
}

Để biết thêm thông tin về drag session xem UICollectionViewDragDelegate.

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

Để nhận dữ liệu từ một drag session từ UICollectionView, 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, giả sử ứng dụng này chỉ tiếp nhận các đối tượng có kiểu là NSString.

func collectionView(_ collectionView: UICollectionView, 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 collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
    // The .move operation is available only for dragging within a single app.
    if tableView.hasActiveDrag {
        if session.items.count > 1 {
            return UICollectionViewDropProposal(operation: .cancel)
        } else {
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        }
    } else {
        return UICollectionViewDropProposal(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 collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) 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 UICollectionView

func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
    let destinationIndexPath: IndexPath
    
    if let indexPath = coordinator.destinationIndexPath {
        destinationIndexPath = indexPath
    } else {
        // Get last index path of collection view.
        let section = collectionView.numberOfSections - 1
        let row = collectionView.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)
        }

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

Tổng kết

Đây là bài viết cuối cùng trong seri 3 phần về Drag & Drop trên iOS 11. Cảm ơn các bạn đã theo dõi.