SwiftUI |使用onDrag和onDrop对单个LazyGrid中的项目重新排序?

时间:2020-06-27 06:53:34

标签: ios swift swiftui collectionview xcode12

我想知道是否可以使用View.onDragView.onDrop在一个LazyGrid中手动添加拖放重新排序?

尽管我可以使用onDrag使每个Item都可拖动,但我不知道如何实现删除部分。

这是我正在尝试的代码:

import SwiftUI

//MARK: - Data

struct Data: Identifiable {
    let id: Int
}

//MARK: - Model

class Model: ObservableObject {
    @Published var data: [Data]
    
    let columns = [
        GridItem(.fixed(160)),
        GridItem(.fixed(160))
    ]
    
    init() {
        data = Array<Data>(repeating: Data(id: 0), count: 100)
        for i in 0..<data.count {
            data[i] = Data(id: i)
        }
    }
}

//MARK: - Grid

struct ContentView: View {
    @StateObject private var model = Model()
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: model.columns, spacing: 32) {
                ForEach(model.data) { d in
                    ItemView(d: d)
                        .id(d.id)
                        .frame(width: 160, height: 240)
                        .background(Color.green)
                        .onDrag { return NSItemProvider(object: String(d.id) as NSString) }
                }
            }
        }
    }
}

//MARK: - GridItem

struct ItemView: View {
    var d: Data
    
    var body: some View {
        VStack {
            Text(String(d.id))
                .font(.headline)
                .foregroundColor(.white)
        }
    }
}

谢谢!

3 个答案:

答案 0 :(得分:21)

SwiftUI 2.0

这里完成了可能的方法的简单演示(没有做太多调整,因为导致代码像演示一样快速增长)。

demo

重要的一点是:a)重新排序不假定等待下降,因此应实时跟踪; b)为避免与坐标共舞,处理逐格项目视图更为简单; c)找到要移动的内容并在数据模型中执行此操作,因此SwiftUI自己为视图设置动画。

通过Xcode 12b3 / iOS 14测试

import SwiftUI
import UniformTypeIdentifiers

struct GridData: Identifiable, Equatable {
    let id: Int
}

//MARK: - Model

class Model: ObservableObject {
    @Published var data: [GridData]

    let columns = [
        GridItem(.fixed(160)),
        GridItem(.fixed(160))
    ]

    init() {
        data = Array(repeating: GridData(id: 0), count: 100)
        for i in 0..<data.count {
            data[i] = GridData(id: i)
        }
    }
}

//MARK: - Grid

struct DemoDragRelocateView: View {
    @StateObject private var model = Model()

    @State private var dragging: GridData?

    var body: some View {
        ScrollView {
           LazyVGrid(columns: model.columns, spacing: 32) {
                ForEach(model.data) { d in
                    GridItemView(d: d)
                        .overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
                        .onDrag {
                            self.dragging = d
                            return NSItemProvider(object: String(d.id) as NSString)
                        }
                        .onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
                }
            }.animation(.default, value: model.data)
        }
    }
}

struct DragRelocateDelegate: DropDelegate {
    let item: GridData
    @Binding var listData: [GridData]
    @Binding var current: GridData?

    func dropEntered(info: DropInfo) {
        if item != current {
            let from = listData.firstIndex(of: current!)!
            let to = listData.firstIndex(of: item)!
            if listData[to].id != current!.id {
                listData.move(fromOffsets: IndexSet(integer: from),
                    toOffset: to > from ? to + 1 : to)
            }
        }
    }

    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }

    func performDrop(info: DropInfo) -> Bool {
        self.current = nil
        return true
    }
}

//MARK: - GridItem

struct GridItemView: View {
    var d: GridData

    var body: some View {
        VStack {
            Text(String(d.id))
                .font(.headline)
                .foregroundColor(.white)
        }
        .frame(width: 160, height: 240)
        .background(Color.green)
    }
}

编辑

以下是当拖放到任何网格项目之外时永不消失的拖动项目的方法:

struct DropOutsideDelegate: DropDelegate { 
    @Binding var current: GridData?  
        
    func performDrop(info: DropInfo) -> Bool {
        current = nil
        return true
    }
}
struct DemoDragRelocateView: View {
    ...

    var body: some View {
        ScrollView {
            ...
        }
        .onDrop(of: [UIType.text], delegate: DropOutsideDelegate(current: $dragging))
    }
}

答案 1 :(得分:2)

上述出色的解决方案还提出了一些额外的问题,所以这是我在 1 月 1 日可以想到的解决方案(即为自己不够雄辩而道歉):

  1. 如果您选择一个网格项并释放它(取消),则视图不会重置

我添加了一个 bool 来检查视图是否已被拖动,如果还没有,那么它首先不会隐藏视图。这有点像黑客,因为它并没有真正重置,它只是推迟隐藏视图,直到它知道您想要拖动它。 IE。如果拖动速度非常快,您可以在视图隐藏之前短暂地看到它。

  1. 如果您将 griditem 放在视图之外,则视图不会重置

通过添加 dropOutside 委托已经部分解决了这个问题,但是除非您有背景视图(如颜色),否则 SwiftUI 不会触发它,我认为这会引起一些混乱。因此,我添加了一个灰色背景来说明如何正确触发它。

希望对大家有所帮助:

import SwiftUI
import UniformTypeIdentifiers

struct GridData: Identifiable, Equatable {
    let id: String
}

//MARK: - Model

class Model: ObservableObject {
    @Published var data: [GridData]

    let columns = [
        GridItem(.flexible(minimum: 60, maximum: 60))
    ]

    init() {
        data = Array(repeating: GridData(id: "0"), count: 50)
        for i in 0..<data.count {
            data[i] = GridData(id: String("\(i)"))
        }
    }
}

//MARK: - Grid

struct DemoDragRelocateView: View {
    @StateObject private var model = Model()

    @State private var dragging: GridData? // I can't reset this when user drops view ins ame location as drag started
    @State private var changedView: Bool = false

    var body: some View {
        VStack {
            ScrollView(.vertical) {
               LazyVGrid(columns: model.columns, spacing: 5) {
                    ForEach(model.data) { d in
                        GridItemView(d: d)
                            .opacity(dragging?.id == d.id && changedView ? 0 : 1)
                            .onDrag {
                                self.dragging = d
                                changedView = false
                                return NSItemProvider(object: String(d.id) as NSString)
                            }
                            .onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging, changedView: $changedView))
                            
                    }
                }.animation(.default, value: model.data)
            }
        }
        .frame(maxWidth:.infinity, maxHeight: .infinity)
        .background(Color.gray.edgesIgnoringSafeArea(.all))
        .onDrop(of: [UTType.text], delegate: DropOutsideDelegate(current: $dragging, changedView: $changedView))
    }
}

struct DragRelocateDelegate: DropDelegate {
    let item: GridData
    @Binding var listData: [GridData]
    @Binding var current: GridData?
    @Binding var changedView: Bool
    
    func dropEntered(info: DropInfo) {
        
        if current == nil { current = item }
        
        changedView = true
        
        if item != current {
            let from = listData.firstIndex(of: current!)!
            let to = listData.firstIndex(of: item)!
            if listData[to].id != current!.id {
                listData.move(fromOffsets: IndexSet(integer: from),
                    toOffset: to > from ? to + 1 : to)
            }
        }
    }

    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }

    func performDrop(info: DropInfo) -> Bool {
        changedView = false
        self.current = nil
        return true
    }
    
}

struct DropOutsideDelegate: DropDelegate {
    @Binding var current: GridData?
    @Binding var changedView: Bool
        
    func dropEntered(info: DropInfo) {
        changedView = true
    }
    func performDrop(info: DropInfo) -> Bool {
        changedView = false
        current = nil
        return true
    }
}

//MARK: - GridItem

struct GridItemView: View {
    var d: GridData

    var body: some View {
        VStack {
            Text(String(d.id))
                .font(.headline)
                .foregroundColor(.white)
        }
        .frame(width: 60, height: 60)
        .background(Circle().fill(Color.green))
    }
}

答案 2 :(得分:1)

这是实现放下部分的方法。但是请记住,如果数据符合ondrop,则UTType可以允许从应用程序外部放入内容。有关UTTypes的更多信息。

将onDrop实例添加到您的lazyVGrid。

           LazyVGrid(columns: model.columns, spacing: 32) {
                ForEach(model.data) { d in
                    ItemView(d: d)
                        .id(d.id)
                        .frame(width: 160, height: 240)
                        .background(Color.green)
                        .onDrag { return NSItemProvider(object: String(d.id) as NSString) }
                }
            }.onDrop(of: ["public.plain-text"], delegate: CardsDropDelegate(listData: $model.data))

创建一个DropDelegate来处理具有给定视图的放置内容和放置位置。

struct CardsDropDelegate: DropDelegate {
    @Binding var listData: [MyData]

    func performDrop(info: DropInfo) -> Bool {
        // check if data conforms to UTType
        guard info.hasItemsConforming(to: ["public.plain-text"]) else {
            return false
        }
        let items = info.itemProviders(for: ["public.plain-text"])
        for item in items {
            _ = item.loadObject(ofClass: String.self) { data, _ in
                // idea is to reindex data with dropped view
                let index = Int(data!)
                DispatchQueue.main.async {
                        // id of dropped view
                        print("View Id dropped \(index)")
                }
            }
        }
        return true
    }
}

performDrop唯一真正有用的参数是放置位置的CGPoint,info.location,将CGPoint映射到要替换的视图似乎是不合理的。我认为OnMove是一个更好的选择,它将使您轻松移动数据/视图。我无法让OnMoveLazyVGrid内工作。

由于LazyVGrid仍处于测试阶段,因此势必会发生变化。我会放弃在更复杂的任务上使用。