Приведи пример мощной нативной фичи iOS для взаимодействия с пользователем.

Ответ

Drag and Drop API для UICollectionView и UITableView — мощная системная фича, позволяющая пользователям интуитивно переупорядочивать контент внутри приложения и между приложениями.

Что позволяет делать:

  • Перетаскивание ячеек внутри одной коллекции/таблицы.
  • Перенос элементов между разными коллекциями/таблицами.
  • Межпрограммный перенос данных между приложениями (например, изображение из Фото в ваше приложение).
  • Поддержка мультитач-жестов для выбора и переноса нескольких элементов сразу.

Базовая реализация для UICollectionView:

  1. Активация:

    collectionView.dragInteractionEnabled = true
    collectionView.dragDelegate = self
    collectionView.dropDelegate = self
  2. Начало перетаскивания (Drag Delegate):

    func collectionView(_ collectionView: UICollectionView,
                       itemsForBeginning session: UIDragSession,
                       at indexPath: IndexPath) -> [UIDragItem] {
        let item = dataModel.item(at: indexPath)
        let itemProvider = NSItemProvider(object: item.title as NSString)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = item // Контекст для внутреннего переноса
        return [dragItem]
    }
  3. Обработка "дропа" (Drop Delegate):

    func collectionView(_ collectionView: UICollectionView,
                       performDropWith coordinator: UICollectionViewDropCoordinator) {
        let destinationIndexPath = coordinator.destinationIndexPath ?? ...
    
        for item in coordinator.items {
            if let sourceIndexPath = item.sourceIndexPath {
                // ВНУТРЕННИЙ ПЕРЕНОС: перемещаем объект в модели данных
                collectionView.performBatchUpdates {
                    let movedItem = dataModel.remove(at: sourceIndexPath)
                    dataModel.insert(movedItem, at: destinationIndexPath)
                    collectionView.moveItem(at: sourceIndexPath, to: destinationIndexPath)
                }
            } else {
                // МЕЖПРОГРАММНЫЙ ПЕРЕНОС: создаем новый объект из данных
                item.dragItem.itemProvider.loadObject(ofClass: NSString.self) { (data, error) in
                    if let text = data as? String {
                        DispatchQueue.main.async {
                            self.dataModel.insert(Item(title: text), at: destinationIndexPath)
                            collectionView.insertItems(at: [destinationIndexPath])
                        }
                    }
                }
            }
        }
    }

Ключевые преимущества:

  • Нативные жесты и анимации (поднятие ячейки, кастомный превью).
  • Высокая производительность, интегрированная с системой.
  • Универсальность: работает с любыми типами данных через NSItemProvider.

Ответ 18+ 🔞

Ну что за тема, блядь! Сидишь такой, пишешь приложение, а тут бац — пользователь хочет пальцем по экрану поводить и всё переставить, как ему вздумается. И ведь система уже всё придумала, эти ваши Drag and Drop API, но пока в них не влезешь, нихуя не понятно, как эта магия работает.

Смотри, вот тебе расклад по полочкам, без этой охуевшей документации от Apple, которая читается как инструкция к шаманскому бубну.

Про что речь вообще? Это ж целый цирк, ёпта! Пользователь может:

  • Таскать ячейки туда-сюда внутри одной таблицы или коллекции — обычное дело.
  • Перекидывать их между разными таблицами или коллекциями — уже интереснее.
  • Вообще, блядь, тащить хуй знает что из другого приложения прямо к тебе! Картинку из «Фоток», текст из «Заметок» — вот это уже мощь.
  • И да, можно сразу несколько штук пальцами обцепить и потащить — выглядит, как будто ты директор склада на сенсорном экране.

Как это впихнуть в UICollectionView? Всё проще, чем кажется, хоть и звучит страшно.

  1. Включи мозг и свитч. Первым делом говоришь коллекции: «Проснись, сука, мы будем таскать!»

    collectionView.dragInteractionEnabled = true // Главный тумблер!
    collectionView.dragDelegate = self   // Тот, кто начнёт таскание
    collectionView.dropDelegate = self   // Тот, кто примет упавшее
  2. Ловим момент, когда пользователь зацепил ячейку (Drag Delegate). Тут надо систему накормить данными, которые потащим. Главное — не обосраться с NSItemProvider.

    func collectionView(_ collectionView: UICollectionView,
                       itemsForBeginning session: UIDragSession,
                       at indexPath: IndexPath) -> [UIDragItem] {
        // 1. Достаём из своей модели данных то, что тащим
        let item = dataModel.item(at: indexPath)
    
        // 2. Заворачиваем это в "посылку", которую система поймёт
        let itemProvider = NSItemProvider(object: item.title as NSString)
    
        // 3. Создаём сам "перетаскиваемый предмет"
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = item // Вот это, бля, важно! Пометка для ВНУТРЕННЕГО переноса.
        return [dragItem] // Отдаём системе один предмет (можно и несколько)
    }
  3. Принимаем "посылку", когда её бросили (Drop Delegate). А вот тут уже настоящая логика, где нужно решить: это перестановка внутри или нам подкинули что-то новое с улицы?

    func collectionView(_ collectionView: UICollectionView,
                       performDropWith coordinator: UICollectionViewDropCoordinator) {
        // Куда бросили? Если не попали в ячейку, придумываем куда (например, в конец).
        let destinationIndexPath = coordinator.destinationIndexPath ?? ...
    
        // Разбираем всё, что нам набросали
        for item in coordinator.items {
            // Если есть sourceIndexPath — значит, тащили ИЗ ЭТОЙ ЖЕ коллекции!
            if let sourceIndexPath = item.sourceIndexPath {
                // ВНУТРЕННИЙ ПЕРЕНОС:
                collectionView.performBatchUpdates {
                    // 1. Выдёргиваем объект из модели
                    let movedItem = dataModel.remove(at: sourceIndexPath)
                    // 2. Втыкаем его на новое место
                    dataModel.insert(movedItem, at: destinationIndexPath)
                    // 3. Говорим коллекции анимировать это движение
                    collectionView.moveItem(at: sourceIndexPath, to: destinationIndexPath)
                }
            } else {
                // А вот это уже МЕЖПРОГРАММНЫЙ ПЕРЕНОС! Нам подсунули что-то извне.
                // Качаем данные из этой "посылки" (itemProvider)
                item.dragItem.itemProvider.loadObject(ofClass: NSString.self) { (data, error) in
                    if let text = data as? String {
                        DispatchQueue.main.async {
                            // Создаём новый объект в нашей модели
                            self.dataModel.insert(Item(title: text), at: destinationIndexPath)
                            // И просим коллекцию красиво его показать
                            collectionView.insertItems(at: [destinationIndexPath])
                        }
                    }
                }
            }
        }
    }

И в чём, блядь, соль-то?

  • Всё уже готово. Жесты, анимация поднятия ячейки, превью — не надо это самому городить, ёпта.
  • Летает быстро. Системная интеграция, не самопал на костылях.
  • Универсально как хуй с винтом. Через NSItemProvider можно передать почти что угодно — строку, картинку, файл, даже свой кастомный тип, если его правильно описать.

Вот и вся магия. Сначала кажется, что нихуя не понятно, но как разберёшься — оказывается, логично и даже удобно. Главное — не забыть про localObject для внутренних перемещений, а то будешь потом ебаться, почему данные слетают.