首页 > 解决方案 > 当我使用 Core Data 在 Swift 中拖放项目和标题时如何对它们进行排序,而 SourceIndexPath 似乎不起作用?

问题描述

当我使用 Core Data 在 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 && 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 项目发给你。

标签: swiftxcode

解决方案


在我的应用程序中,我采用以下方式:

  1. 获取当前对象列表,按核心数据记录排序
  2. UICollectionViewDragDelegate从(collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath))中删除具有 indexPath 的对象
  3. UICollectionViewDropDelegate将此对象从(collectionView(_collectionView:UICollectionView,performDropWith coordinator:UICollectionViewDropCoordinator))重新插入到目标indexPath的列表中
  4. 使用更新的排序索引更新 Core Data 值索引

代码示例:

// 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")
    }
}

(强制展开已完成,因为如果没有日历,则无法启动丢弃会话)

之后我的模型被更新,我可以将新快照应用到集合视图。我认为您应该在一个地方订购您的对象(例如,当您应用快照时)并且在拖放会话中您只需要更改您的数据模型并且不要触摸集合视图快照。

您可以对 Headers 和 Childs 使用相同的方法 - 唯一的区别在于您将在第 1 点中使用的对象列表


推荐阅读