Как я могу сортировать свои элементы и заголовки, когда я перетаскиваю их в Swift с помощью основных данных, а sourceIndexPath, похоже, не работает?

#swift #xcode

Вопрос:

Как я могу правильно сортировать свои элементы и заголовки при перетаскивании их в Swift с использованием основных данных и получать правильный элемент при использовании пути к индексу?

Проблема, с которой я сталкиваюсь, заключается в том, что мой алгоритм сортировки неправильно извлекает правильные строки и разделы в моем представлении коллекции (я получаю ошибки индекса вне диапазона в строке с функцией stride). Я хотел бы иметь возможность перетаскивать строки и удалять их вместе с их порядком для сохранения, а также перетаскивать заголовки, чтобы изменить их порядок и сохранить их порядок.

Проблема, по-видимому, возникает при присвоении значения «верхней» и «нижней» переменным, а также при переходе к получению значения для свойства «Новый порядок» моей основной сущности данных. Похоже, что при попытке получить перетаскиваемый элемент с помощью sourceIndexPath.section и sourceIndexPath.item возникает проблема.

Посмотрите, где я пытаюсь заставить перетаскивание работать.

 import UIKit


class DetailViewController: UIViewController {
    
    var fromItem: Item
    
    init(item: Item) {
        self.fromItem = item
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    var dataSourceSnapshot = NSDiffableDataSourceSnapshot<Header, ListData>()
    
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Header, ListData>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemGroupedBackground
        
        var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        layoutConfig.headerMode = .firstItemInSection
        let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
        
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: listLayout)
        view.addSubview(collectionView)
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 0.0),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
        ])
        
        collectionView.dragDelegate = self
        collectionView.dropDelegate = self
        collectionView.dragInteractionEnabled = true
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewHeader))
       
        let headers = fromItem.headers?.allObjects as? [Header] ?? []
        print("headers here (headers)")
        dataSourceSnapshot.appendSections(headers.sorted(by: {$0.order < $1.order}))
        dataSource.apply(dataSourceSnapshot)
        
        for header in headers.sorted(by: {$0.order < $1.order}) {
            
            let children = header.children!.allObjects as? [Child] ?? []
            let sortedChildren = children.sorted{$0.order > $1.order}
            
            var dataSourceSnapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
            let headerItem = ListData.header(header)
            dataSourceSnapshot.append([headerItem])
            
            for child in sortedChildren {
                dataSourceSnapshot.append([.child(child)], to: headerItem)
            }
            
            dataSourceSnapshot.expand([headerItem])
            
            dataSource.apply(dataSourceSnapshot, to: header, animatingDifferences: false)
        }
    }
    
    @objc func addNewHeader() {
        var order: Double = 0
        let header = fromItem.headers?.allObjects as? [Header] ?? []
        if header.count > 0 {
            order = header.last!.order   25
        } else {
            order = 100
        }
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        let context = appDelegate.persistentContainer.viewContext
        
        let newHeader = Header(context: context)
        newHeader.name = "Header"
        newHeader.id = UUID()
        newHeader.order = order
        newHeader.item = fromItem
        
        var childOrder: Double = 0
        let firstChild = header.first(where: {$0.id == newHeader.id })?.children?.allObjects as? [Child] ?? []
        
        if header.count > 0 amp;amp; firstChild.count > 0 {
            childOrder = firstChild.last!.order   25
        } else {
            childOrder = 100
        }

        
        var snapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
        
        dataSourceSnapshot.appendSections([newHeader])
        
        let headerItem = ListData.header(newHeader)
        snapshot.append([headerItem])
        
        var childOrderIncrement = childOrder
        for i in 1...4 {
            let child = Child(context: context)
            child.order = childOrderIncrement
            child.name = "child (i)"
            child.id = UUID()
            child.header = newHeader
            snapshot.append([.child(child)], to: headerItem)
            childOrderIncrement  = 25
        }
        
        snapshot.expand([headerItem])
        
        dataSource.apply(snapshot, to: newHeader, animatingDifferences: false)
        try? context.save()
        
        print("Headers = (fromItem.headers?.allObjects as? [Header] ?? [])")
    }
}

extension DetailViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let item = dataSource.itemIdentifier(for: indexPath) else {
            collectionView.deselectItem(at: indexPath, animated: true)
            return [UIDragItem]()
        }
        
        switch item {
        case .header(let header):
            let itemProvider = NSItemProvider(object: header.objectID.uriRepresentation().absoluteURL as NSURL)
            
            let dragItem = UIDragItem(itemProvider: itemProvider)
            
            return [dragItem]
            
        case .child(let child):
            let itemProvider = NSItemProvider(object: NSString(string: child.id!.uuidString))
            
            let dragItem = UIDragItem(itemProvider: itemProvider)
            
            return [dragItem]
        }
    }
    
    
func DetailViewController(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
    guard let item = dataSource.itemIdentifier(for: indexPath) else {
        collectionView.deselectItem(at: indexPath, animated: true)
        return [UIDragItem]()
    }
    
    switch item {
    case .header(_):
        
        return [UIDragItem]()
    case .child(let child):
        let itemProvider = NSItemProvider(object: NSString(string: child.id!.uuidString))
        
        let dragItem = UIDragItem(itemProvider: itemProvider)
        
        return [dragItem]
    }
}
}

extension DetailViewController: UICollectionViewDropDelegate {
    
    private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
        let items = coordinator.items
        if items.count == 1, let item = items.first, let sourceIndexPath = item.sourceIndexPath {
            var destIndexPath = destinationIndexPath
            if destIndexPath.item >= collectionView.numberOfItems(inSection: destIndexPath.section) {
                destIndexPath.item = collectionView.numberOfItems(inSection: destIndexPath.section) - 1
            }
            guard let fromChild = dataSource.itemIdentifier(for: sourceIndexPath),
                  sourceIndexPath != destIndexPath else { return }
            
            var snap = dataSource.snapshot()
            let header = dataSource.sectionIdentifier(for: destIndexPath.section)!
            let sourceHeader = dataSource.sectionIdentifier(for: sourceIndexPath.section)!
            
            
            switch fromChild {
            case .header(_):
                print("cannot reorder headers here")
            case .child(let fromChild):
                snap.deleteItems([.child(fromChild)])
                
                if let toChild = dataSource.itemIdentifier(for: destIndexPath) {
                    switch toChild {
                    case .header(_):
                        print("cannot reorder headers here")
                        
                    case .child(let toChild):
                        let isAfter = destIndexPath.item > sourceIndexPath.item
                        if isAfter {
                            if header == sourceHeader {
                                snap.insertItems([.child(fromChild)], afterItem: .child(toChild))
                                
                            } else {
                                snap.insertItems([.child(fromChild)], afterItem: .child(toChild))
                                
                            }
                        } else {
                            snap.insertItems([.child(fromChild)], beforeItem: .child(toChild))
                        }
                    }
                } else {
                    snap.appendItems([.child(fromChild)], toSection: header)
                }
                
            }
            
            var upper: Double
            var lower: Double
            
            let headers = fromItem.headers?.allObjects as? [Header] ?? []
            let children = headers[destIndexPath.section].children!.allObjects as? [Child] ?? []
            let sortedChildren = children.sorted{$0.order > $1.order}
            
            
            if destIndexPath.item == sortedChildren.count {
                print("Appending to the end of the list")
                lower = sortedChildren.last?.order ?? 0
                upper = sortedChildren.last?.order ?? 0   100.0
            } else if destIndexPath.item == 0 {
                print("Inserting into the beginning")
                lower = 0.0
                
                upper = sortedChildren.first?.order ?? 100.0
            } else {
                print("Inserting into the middle of the list")
                
                upper = sortedChildren[destIndexPath.item - 2].order
                lower = sortedChildren[destIndexPath.item - 1].order
            }
            print(upper)
            print(lower)
            
            
            if header != sourceHeader {
                
                sortedChildren[sourceIndexPath.item].header = header
            }
            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
            appDelegate.saveContext()
            
            dataSource.apply(snap, animatingDifferences: false)
            
            for header in headers.sorted(by: {$0.order < $1.order}) {
                
                let children = header.children!.allObjects as? [Child] ?? []
                let sortedChildren = children.sorted{$0.order > $1.order}
                
                var dataSourceSnapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
                let headerItem = ListData.header(header)
                dataSourceSnapshot.append([headerItem])
                
                for child in sortedChildren {
                    dataSourceSnapshot.append([.child(child)], to: headerItem)
                }
                
                dataSourceSnapshot.expand([headerItem])
                
                dataSource.apply(dataSourceSnapshot, to: header, animatingDifferences: false)
            }
        }
    }
    
    private func reorderHeaders(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
        let items = coordinator.items
        if items.count == 1, let item = items.first, let sourceIndexPath = item.sourceIndexPath {
            
            guard let fromHeader = dataSource.sectionIdentifier(for: sourceIndexPath.section),
                  sourceIndexPath != destinationIndexPath else { return }
            
            var snap = dataSource.snapshot()
            
            let unsortedHeaders = fromItem.headers?.allObjects as? [Header] ?? []
            let headers = unsortedHeaders.sorted{$0.order > $1.order}
            
            let children = fromHeader.children!.allObjects as? [Child] ?? []
            let sortedChildren = children.sorted{$0.order > $1.order}

            for i in sortedChildren {
                snap.deleteItems([.child(i)])
            }
            snap.deleteSections([fromHeader])
            
            if let toHeader = dataSource.sectionIdentifier(for: destinationIndexPath.section) {
                let isAfter = destinationIndexPath.section > sourceIndexPath.section
                
                if isAfter {
                    snap.insertSections([fromHeader], afterSection: toHeader)
                } else {
                    snap.insertSections([fromHeader], beforeSection: toHeader)
                }
                
            } else {
                snap.appendSections([fromHeader])
            }
            
            var upper: Double
            var lower: Double
            
            if destinationIndexPath.item == headers.count {
                print("Appending to the end of the list")
                lower = headers.last!.order
                upper = headers.last!.order   100.0
            } else if destinationIndexPath.item == 0 {
                print("Inserting into the beginning")
                lower = 0.0
                upper = headers.first?.order ?? 100.0
            } else {
                print("Inserting into the middle of the list")
                upper = headers[destinationIndexPath.section - 1].order
                lower = headers[destinationIndexPath.section].order
            }
            
            headers[sourceIndexPath.section].order = stride(from: lower, to: upper, by: (upper - lower) / Double(2)).map{ $0 }[1]
            
            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
            appDelegate.saveContext()
            
            for header in headers.sorted(by: {$0.order < $1.order}) {
                
                let children = header.children!.allObjects as? [Child] ?? []
                let sortedChildren = children.sorted{$0.order > $1.order}
                
                var dataSourceSnapshot = NSDiffableDataSourceSectionSnapshot<ListData>()
                let headerItem = ListData.header(header)
                dataSourceSnapshot.append([headerItem])
                
                for child in sortedChildren {
                    dataSourceSnapshot.append([.child(child)], to: headerItem)
                }
                
                dataSourceSnapshot.expand([headerItem])
                
                dataSource.apply(dataSourceSnapshot, to: header, animatingDifferences: false)
            }
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        let destinationIndexPath: IndexPath
        
        if let indexPath = coordinator.destinationIndexPath {
            destinationIndexPath = indexPath
        } else {
            let section = collectionView.numberOfSections - 1
            let row = collectionView.numberOfItems(inSection: section)
            destinationIndexPath = IndexPath(item: row, section: section)
        }
        
        switch coordinator.proposal.operation {
        case .move:
            for item in coordinator.items {
                if item.dragItem.localObject as! String == "child" {
                    coordinator.session.loadObjects(ofClass: NSString.self) { items in
                        

                        DispatchQueue.main.async {
                            print("reordering the children")
                            
                            self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
                        }
                }
                } else {
                    coordinator.session.loadObjects(ofClass: NSURL.self) { items in
                        
                        DispatchQueue.main.async {
                            print("reordering the headers")
                            
                            self.reorderHeaders(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
                        }
                    }
                }
            }
            
            break
        case .copy:
            return
        default:
            return
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
    }
    
    
}
 

Моя Основная Модель Данных:

введите описание изображения здесь

введите описание изображения здесь

Если у вас есть еще какие-либо вопросы, пожалуйста, не стесняйтесь задавать их.

Если бы это было проще, я мог бы отправить вам весь свой проект Xcode.

Ответ №1:

В своих приложениях я делаю это следующим образом:

  1. получить текущий список объектов, упорядоченных по основной записи данных
  2. удалите объект с выражением из UICollectionViewDragDelegate (CollectionView(_ CollectionView: UICollectionView, элементы для начальной сессии: UIDragSession, в indexPath: indexPath))
  3. вставьте этот объект обратно в список в пункте назначения indexPath из UICollectionViewDropDelegate (CollectionView(_ CollectionView: UICollectionView, координатор выполнения: UICollectionViewDropCoordinator))
  4. обновите индексы значений основных данных с помощью обновленных индексов сортировки

Пример кода:

 // MARK: - UICollectionViewDragDelegate
extension CombinedMainVC: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let cell = collectionView.cellForItem(at: indexPath) as? CalendarCC, let item = cell.calendar, let name = item.name else { return [UIDragItem]() }
        startingIndexPath = indexPath
        let itemProvider = NSItemProvider(object: name as NSString)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        dragItem.localObject = item
        return [dragItem]
    }
}

// MARK: - UICollectionViewDropDelegate
extension CombinedMainVC: UICollectionViewDropDelegate {
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        let forbidden = UICollectionViewDropProposal(operation: .forbidden)
        guard destinationIndexPath?.section == startingIndexPath.section else { return forbidden }
        return collectionView.hasActiveDrag ? UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) : forbidden
    }
    
    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        guard let destinationIndexPath = coordinator.destinationIndexPath else { return }
        
        var calendars = fetchedRC.fetchedObjects!
        let list = calendars.remove(at: startingIndexPath.row)
        calendars.insert(list, at: destinationIndexPath.row)
        
        for cal in fetchedRC.fetchedObjects! {
            let sortIndex = calendars.firstIndex(of: cal)!
            cal.secondData = Int32(sortIndex)
        }
        
        updated = false
        appDelegate.saveContext()
        postNotification("dateChanged")
    }
}
 

(принудительное разворачивание выполняется, потому что сеанс удаления не может быть запущен, если нет календарей)

После этого моя модель будет обновлена, и я смогу применить новый снимок к представлению коллекции. Я думаю, что вы должны упорядочить свои объекты в одном месте (например, при применении моментальных снимков), а в сеансе перетаскивания вам нужно просто изменить модель данных и не трогать снимок представления коллекции.

Вы могли бы использовать тот же подход для заголовков и дочерних элементов — разница только в списке объектов, которые вы будете использовать в пункте 1

Комментарии:

1. Спасибо тебе, Виктор. Не могли бы вы взглянуть на мой код выше, чтобы узнать, верна ли моя логика при добавлении в конец, вставке в середине или вставке в начале списка просмотра моей коллекции. Если это поможет, я мог бы отправить вам весь свой проект Xcode.

2. @Найтхаук Уверен

3. Спасибо, Виктор. Если вам нужна копия моего проекта Xcode, есть ли у вас электронное письмо, на которое я могу его отправить? Моя главная проблема, похоже, заключается в получении правильного элемента с использованием пути назначения и исходного индекса. Я не совсем понимаю, что я делаю не так.

4. mail@viktorgavrilov.com

5. только что отправил тебе электронное письмо.