[英]SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?
I was wondering if it is possible to use the View.onDrag
and View.onDrop
to add drag and drop reordering within one LazyGrid
manually?我想知道是否可以使用View.onDrag
和View.onDrop
在一个LazyGrid
手动添加拖放重新排序?
Though I was able to make every Item draggable using onDrag
, I have no idea how to implement the dropping part.虽然我能够使用onDrag
使每个 Item 可拖动,但我不知道如何实现放置部分。
Here is the code I was experimenting with:这是我正在试验的代码:
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)
}
}
}
Thank you!谢谢!
SwiftUI 2.0 SwiftUI 2.0
Here is completed simple demo of possible approach (did not tune it much, `cause code growing fast as for demo).这是可能方法的完整简单演示(没有对其进行太多调整,因为代码增长速度与演示一样快)。
Important points are: a) reordering does not suppose waiting for drop, so should be tracked on the fly;要点是: a) 重新排序不假设等待丢弃,因此应动态跟踪; b) to avoid dances with coordinates it is more simple to handle drop by grid item views; b)为了避免与坐标跳舞,处理网格项目视图的拖放更简单; c) find what to where move and do this in data model, so SwiftUI animate views by itself. c) 在数据 model 中找到要移动的内容并执行此操作,因此 SwiftUI 会自行为视图设置动画。
Tested with Xcode 12b3 / iOS 14用 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)
}
}
Here is how to fix the never disappearing drag item when dropped outside of any grid item:以下是如何在拖放到任何网格项目之外时修复永不消失的拖动项目:
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: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
}
}
There was a few additional issues raised to the excellent solutions above, so here's what I could come up with on Jan 1st with a hangover (ie apologies for being less than eloquent):上面的优秀解决方案还引发了一些额外的问题,所以这是我在 1 月 1 日宿醉时能想到的(即为不够雄辩而道歉):
I added a bool that checks if the view had been dragged yet, and if it hasn't then it doesn't hide the view in the first place.我添加了一个 bool 来检查视图是否已被拖动,如果还没有,那么它不会首先隐藏视图。 It's a bit of a hack, because it doesn't really reset, it just postpones hiding the view until it knows that you want to drag it.这有点像黑客,因为它并没有真正重置,它只是推迟隐藏视图,直到它知道你想要拖动它。 Ie if you drag really fast, you can see the view briefly before it's hidden.即,如果你拖得非常快,你可以在它被隐藏之前短暂地看到它。
This one was partially addressed already, by adding the dropOutside delegate, but SwiftUI doesn't trigger it unless you have a background view (like a color), which I think caused some confusion.通过添加 dropOutside 代表已经部分解决了这个问题,但是 SwiftUI 不会触发它,除非您有背景视图(如颜色),我认为这会引起一些混乱。 I therefore added a background in grey to illustrate how to properly trigger it.因此,我添加了一个灰色背景来说明如何正确触发它。
Hope this helps anyone:希望这可以帮助任何人:
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))
}
}
Here's my solution (based on Asperi's answer) for those who seek for a generic approach for ForEach
where I abstracted the view away :这是我的解决方案(基于 Asperi 的回答),适用于那些为ForEach
寻求通用方法的人,我在其中抽象了视图:
struct ReorderableForEach<Content: View, Item: Identifiable & Equatable>: View {
let items: [Item]
let content: (Item) -> Content
let moveAction: (IndexSet, Int) -> Void
// A little hack that is needed in order to make view back opaque
// if the drag and drop hasn't ever changed the position
// Without this hack the item remains semi-transparent
@State private var hasChangedLocation: Bool = false
init(
items: [Item],
@ViewBuilder content: @escaping (Item) -> Content,
moveAction: @escaping (IndexSet, Int) -> Void
) {
self.items = items
self.content = content
self.moveAction = moveAction
}
@State private var draggingItem: Item?
var body: some View {
ForEach(items) { item in
content(item)
.overlay(draggingItem == item && hasChangedLocation ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
draggingItem = item
return NSItemProvider(object: "\(item.id)" as NSString)
}
.onDrop(
of: [UTType.text],
delegate: DragRelocateDelegate(
item: item,
listData: items,
current: $draggingItem,
hasChangedLocation: $hasChangedLocation
) { from, to in
withAnimation {
moveAction(from, to)
}
}
)
}
}
}
The DragRelocateDelegate
basically stayed the same, although I made it a bit more generic and safer: DragRelocateDelegate
基本上保持不变,尽管我使它更通用和更安全:
struct DragRelocateDelegate<Item: Equatable>: DropDelegate {
let item: Item
var listData: [Item]
@Binding var current: Item?
@Binding var hasChangedLocation: Bool
var moveAction: (IndexSet, Int) -> Void
func dropEntered(info: DropInfo) {
guard item != current, let current = current else { return }
guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return }
hasChangedLocation = true
if listData[to] != current {
moveAction(IndexSet(integer: from), to > from ? to + 1 : to)
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
current = nil
return true
}
}
ReorderableForEach(items: itemsArr) { item in
SomeFancyView(for: item)
} moveAction: { from, to in
itemsArr.move(fromOffsets: from, toOffset: to)
}
Here is how you implement the on drop part.以下是您如何实现 on drop 部分。 But remember the ondrop
can allow content to be dropped in from outside the app if the data conforms to the UTType
.但是请记住,如果数据符合UTType
, ondrop
可以允许从应用程序外部放入内容。 More on UTTypes .更多关于UTTypes 。
Add the onDrop instance to your lazyVGrid.将 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))
Create a DropDelegate to handling dropped content and the drop location with the given view.创建一个 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
}
}
Also the only real useful parameter of performDrop
is info.location
a CGPoint of the drop location, Mapping a CGPoint to the view you want to replace seems unreasonable. performDrop
唯一真正有用的参数是info.location
放置位置的 CGPoint,将 CGPoint 映射到要替换的视图似乎不合理。 I would think the OnMove
would be a better option and would make moving your data/Views a breeze.我认为OnMove
将是一个更好的选择,并且可以轻松移动您的数据/视图。 I was unsuccessful to get OnMove
working within a LazyVGrid
.我没有成功让OnMove
在LazyVGrid
中工作。
As LazyVGrid
are still in beta and are bound to change.由于LazyVGrid
仍处于测试阶段,并且必然会发生变化。 I would abstain from use on more complex tasks.我会避免使用更复杂的任务。
I was trying to figure out how to leverage this solution in SwiftUI for macOS when dragging icons to re-order a horizontal set of items.在拖动图标以重新排列一组水平项目时,我试图弄清楚如何在 macOS 的 SwiftUI 中利用此解决方案。 Thanks to @ramzesenok and @Asperi for the overall solution.感谢@ramzesenok 和@Asperi 的整体解决方案。 I added a CGPoint property along with their solution to achieve the desired behavior.我添加了一个 CGPoint 属性及其解决方案以实现所需的行为。 See the animation below.请参阅下面的 animation。
Define the point定义点
@State private var drugItemLocation: CGPoint?
I used in dropEntered
, dropExited
, and performDrop
DropDelegate functions.我在dropEntered
、 dropExited
和performDrop
DropDelegate 函数中使用过。
func dropEntered(info: DropInfo) {
if current == nil {
current = item
drugItemLocation = info.location
}
guard item != current,
let current = current,
let from = icons.firstIndex(of: current),
let toIndex = icons.firstIndex(of: item) else { return }
hasChangedLocation = true
drugItemLocation = info.location
if icons[toIndex] != current {
icons.move(fromOffsets: IndexSet(integer: from), toOffset: toIndex > from ? toIndex + 1 : toIndex)
}
}
func dropExited(info: DropInfo) {
drugItemLocation = nil
}
func performDrop(info: DropInfo) -> Bool {
hasChangedLocation = false
drugItemLocation = nil
current = nil
return true
}
For a full demo, I created a gist using Playgrounds对于完整的演示,我使用 Playgrounds 创建了一个要点
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.