UICollectionView 与 SwiftUI + 拖放重新排序可能吗?

Posted

技术标签:

【中文标题】UICollectionView 与 SwiftUI + 拖放重新排序可能吗?【英文标题】:UICollectionView with SwiftUI + Drag and drop reordering possible? 【发布时间】:2020-05-22 10:31:16 【问题描述】:

我正在我的 SwiftUI 视图中实现一个 UICollectionView(也与 SwiftUI 视图单元格)。 所以我有一个 Hosting+Representable 组合。

现在我想通过拖放重新排列我的单元格,但没有任何反应。

这个想法是将longpressGesture与给定的函数canMoveItemAt和moveItemAt一起使用。

这里是完整的代码:

import SwiftUI
import UIKit

struct ContentView: View 
    var body: some View 
        CollectionComponent()
    


struct CollectionComponent : UIViewRepresentable 
    func makeCoordinator() -> CollectionComponent.Coordinator 
        Coordinator(data: [])
    

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate 
        var data: [String] = []

        init(data: [String]) 

            for index in (0...20) 
                self.data.append("\(index)")
            

        

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
            data.count
        

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
            cell.cellView.rootView = AnyView(CellView(text: data[indexPath.item]))
            return cell
        

        func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool 
            return true
        

        func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 
            print("Changing the cell order, moving: \(sourceIndexPath.row) to \(destinationIndexPath.row)")
        
    

    func makeUIView(context: Context) -> UICollectionView 

        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.itemSize = CGSize(width: 150, height: 150)
        layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.dataSource = context.coordinator
        collectionView.delegate = context.coordinator
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "cell")

        let longPressGesture = UILongPressGestureRecognizer(target: self, action: Selector(("handleLongGesture:")))
        collectionView.addGestureRecognizer(longPressGesture)

        func handleLongGesture(gesture: UILongPressGestureRecognizer) 

            switch(gesture.state) 

            case UIGestureRecognizerState.began:
                guard let selectedIndexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)) else 
                    break
                
                collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
            case UIGestureRecognizerState.changed:
                collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
            case UIGestureRecognizerState.ended:
                collectionView.endInteractiveMovement()
            default:
                collectionView.cancelInteractiveMovement()
            
        

        return collectionView
    

    func updateUIView(_ uiView: UICollectionView, context: Context) 

    



class Cell: UICollectionViewCell 
    public var cellView = UIHostingController(rootView: AnyView(EmptyView()))

    public override init(frame: CGRect) 
        super.init(frame: frame)
        configure()
    

    public required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        configure()
    

    private func configure() 
        contentView.addSubview(cellView.view)

        cellView.view.preservesSuperviewLayoutMargins = false
        cellView.view.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            cellView.view.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
            cellView.view.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
            cellView.view.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
            cellView.view.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
        ])
    


struct CellView: View 
    let text: String

    var body: some View 
        ZStack 
            Text(text)
        
        .frame(width: 150, height: 150)
        .background(Color.blue)
    

谢谢!

【问题讨论】:

使用LazyGrid的完整SwiftUI版本可以在这里找到:***.com/questions/62606907/… 【参考方案1】:

只需使用UICollectionViewDragDelegateUICollectionViewDropDelegate 将单元格视图拖放到UICollectionView 中。它完美地工作。这是示例代码...

struct ContentView: View 
     var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    var body: some View 
        GeometryReader  proxy in
            GridView(self.numbers, proxy: proxy)  number in
                Image("image\(number)")
                .resizable()
                .scaledToFill()
            
        
    

struct GridView<CellView: View>: UIViewRepresentable 
    let cellView: (Int) -> CellView
    let proxy: GeometryProxy
    var numbers: [Int]
    init(_ numbers: [Int], proxy: GeometryProxy, @ViewBuilder cellView: @escaping (Int) -> CellView) 
        self.proxy = proxy
        self.cellView = cellView
        self.numbers = numbers
    
    func makeUIView(context: Context) -> UICollectionView 
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 0
        layout.minimumInteritemSpacing = 0

        let collectionView = UICollectionView(frame: UIScreen.main.bounds, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.register(GridCellView.self, forCellWithReuseIdentifier: "CELL")

        collectionView.dragDelegate = context.coordinator //to drag cell view
        collectionView.dropDelegate = context.coordinator //to drop cell view

        collectionView.dragInteractionEnabled = true
        collectionView.dataSource = context.coordinator
        collectionView.delegate = context.coordinator
        collectionView.contentInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
        return collectionView
    
    func updateUIView(_ uiView: UICollectionView, context: Context)  
    func makeCoordinator() -> Coordinator 
        Coordinator(self)
    
    class Coordinator: NSObject, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDragDelegate, UICollectionViewDropDelegate 
        var parent: GridView
        init(_ parent: GridView) 
            self.parent = parent
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
            return parent.numbers.count
        

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL", for: indexPath) as! GridCellView
            cell.backgroundColor = .clear
            cell.cellView.rootView = AnyView(parent.cellView(parent.numbers[indexPath.row]).fixedSize())
            return cell
        

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 
            return CGSize(width: ((parent.proxy.frame(in: .global).width - 8) / 3), height: ((parent.proxy.frame(in: .global).width - 8) / 3))
        

        //Provides the initial set of items (if any) to drag.
        func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] 
            let item = self.parent.numbers[indexPath.row]
            let itemProvider = NSItemProvider(object: String(item) as NSString)
            let dragItem = UIDragItem(itemProvider: itemProvider)
            dragItem.localObject = item
            return [dragItem]
        

        //Tells your delegate that the position of the dragged data over the collection view changed.
        func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal 
            if collectionView.hasActiveDrag 
                return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
            
            return UICollectionViewDropProposal(operation: .forbidden)
        

        //Tells your delegate to incorporate the drop data into the collection view.
        func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) 
            var destinationIndexPath: IndexPath
            if let indexPath = coordinator.destinationIndexPath 
                destinationIndexPath = indexPath
             else 
                let row = collectionView.numberOfItems(inSection: 0)
                destinationIndexPath = IndexPath(item: row - 1, section: 0)
            
            if coordinator.proposal.operation == .move 
                self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
            
        
        private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) 
            if let item = coordinator.items.first, let sourceIndexPath = item.sourceIndexPath 
                collectionView.performBatchUpdates(
                    self.parent.numbers.remove(at: sourceIndexPath.item)
                    self.parent.numbers.insert(item.dragItem.localObject as! Int, at: destinationIndexPath.item)
                    collectionView.deleteItems(at: [sourceIndexPath])
                    collectionView.insertItems(at: [destinationIndexPath])
                , completion: nil)
                coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
            
        
    

class GridCellView: UICollectionViewCell 
    public var cellView = UIHostingController(rootView: AnyView(EmptyView()))
    public override init(frame: CGRect) 
        super.init(frame: frame)
        configure()
    
    public required init?(coder aDecoder: NSCoder) 
        super.init(coder: aDecoder)
        configure()
    
    private func configure() 
        contentView.addSubview(cellView.view)
        cellView.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            cellView.view.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 5),
            cellView.view.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -5),
            cellView.view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
            cellView.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
        ])
        cellView.view.layer.masksToBounds = true
    

你可以在这里看到最终结果https://media.giphy.com/media/UPuWLauQepwi5Q77PA/giphy.gif 谢谢。 X_X

【讨论】:

嘿,非常感谢您帮助我!这是有效的——唯一的问题是感觉一切都有延迟?你也有这个问题吗?不幸的是,它不够流畅:/ 抱歉回复晚了。是的,我遇到了同样的问题。但是使用UIKit View 代替SwiftUI View 来代替cell view 没有任何延迟。 这里是只使用UIKit Viewmedia.giphy.com/media/JmyxP7KF1qPLx8k45M/giphy.gif的结果

以上是关于UICollectionView 与 SwiftUI + 拖放重新排序可能吗?的主要内容,如果未能解决你的问题,请参考以下文章

UICollectionView 与 UIWebView 大小问题

UICollectionView 水平滚动与静态焦点项目

UIScrollView 与 UICollectionView

UICollectionView(3x3 Grid)与静态单元格分页

UICollectionView 与屏幕上任何位置的单元格

如何将 Plist 文件与 UITableView / UICollectionView 一起使用?