SwiftUI |使用 onDrag 和 onDrop 在一个 LazyGrid 中重新排序项目?
Posted
技术标签:
【中文标题】SwiftUI |使用 onDrag 和 onDrop 在一个 LazyGrid 中重新排序项目?【英文标题】:SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid? 【发布时间】:2020-10-17 18:33:54 【问题描述】:我想知道是否可以使用View.onDrag
和View.onDrop
在一个LazyGrid
中手动添加拖放重新排序?
虽然我可以使用onDrag
使每个项目都可拖动,但我不知道如何实现拖放部分。
这是我正在试验的代码:
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)
谢谢!
【问题讨论】:
【参考方案1】:SwiftUI 2.0
这里完成了可能方法的简单演示(没有进行太多调整,因为代码增长速度与演示一样快)。
重要的一点是: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: [UTType.text], delegate: DropOutsideDelegate(current: $dragging))
【讨论】:
效果很好,但是,当您将拖动状态变量拖出 v-grid 时,它不会重置为 ni,因此覆盖在原始项目上。当您开始拖动时,您拖动的项目将获得白色的叠加颜色。当您将它拖出 v-grid(而不是在另一个项目上),然后释放时,它会移回原始位置。但是,白色覆盖层保持不变,并且拖动永远不会设置为零。我也尝试过使用 Sections 来完成这项工作,但我的第一次尝试失败了。 @user3122959 我已经在 ScrollView 本身上添加了一个 onDrop 以捕获拖动在网格本身之外结束的时间,在执行放置时将拖动设置为 nil。 @jdanthinne 谢谢!这样看起来很棒。 我注意到,当将拖动的项目放在自身上时(以取消意外拖动),它将不可见,因为 dragItem 没有设置为 nil。关于如何实现这一目标的想法? 我认为可以肯定地说,如果 @Asperi 停止回答有关 Stack Overflow 的问题,大多数 SwiftUI 开发将陷入停顿。 ? 这非常有帮助。谢谢!【参考方案2】:对于那些为 ForEach
寻求通用方法的人来说,这是我的解决方案(基于 Asperi 的回答),我在其中抽象了视图:
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)
)
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)
【讨论】:
这太棒了,非常感谢你发布这个!【参考方案3】:上面的优秀解决方案引发了一些额外的问题,所以这是我在 1 月 1 日的宿醉时可以提出的问题(即为不够雄辩而道歉):
-
如果您选择一个网格项并释放它(取消),则视图不会重置
我添加了一个布尔值来检查视图是否已经被拖动,如果还没有,那么它不会首先隐藏视图。这有点像黑客,因为它并没有真正重置,它只是推迟隐藏视图,直到它知道你想要拖动它。 IE。如果你拖得非常快,你可以在它被隐藏之前短暂地看到它。
-
如果将网格项放在视图外,则视图不会重置
通过添加 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))
【讨论】:
【参考方案4】:以下是实现 on drop 部分的方法。但请记住,如果数据符合UTType
,ondrop
可以允许从应用程序外部放入内容。更多关于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
唯一真正有用的参数是info.location
放置位置的 CGPoint,将 CGPoint 映射到要替换的视图似乎不合理。我认为OnMove
将是一个更好的选择,并且可以轻松移动您的数据/视图。我没有成功让OnMove
在LazyVGrid
中工作。
由于LazyVGrid
仍处于测试阶段,并且必然会发生变化。我会避免使用更复杂的任务。
【讨论】:
以上是关于SwiftUI |使用 onDrag 和 onDrop 在一个 LazyGrid 中重新排序项目?的主要内容,如果未能解决你的问题,请参考以下文章