SwiftUI中如何实现多选列表项的拖放功能?
实现SwiftUI列表内多选元素拖放
我需要在SwiftUI列表中实现多选元素拖放到列表内任意位置的功能。目前单个元素的拖放可以正常工作,但多选元素的拖放无法实现;不过如果把列表区域显示两次,从一个列表向另一个列表拖放已选中的元素时,会正常显示放置提示(加号图标)。
解决方案
要实现同列表内的多选拖放,需要调整以下几个核心部分:
1. 让Item支持数组传输
修改Item的Transferable实现,支持单个元素和元素数组的传输,同时让Item遵循Codable简化序列化:
import SwiftUI import UniformTypeIdentifiers struct Item: Identifiable, Codable { let id: UUID var name: String init(id: UUID = UUID(), name: String) { self.id = id self.name = name } } @available(iOS 16, macOS 13, *) extension Item: Transferable { static var transferRepresentation: some TransferRepresentation { // 自定义UTI类型避免冲突 CodableRepresentation(contentType: .singleItem) CodableRepresentation<[Item]>(contentType: .multipleItems) } static let singleItem = UTType(exportedAs: "com.yourdomain.single-item") static let multipleItems = UTType(exportedAs: "com.yourdomain.multiple-items") }
2. 管理多选状态与拖拽逻辑
在ContentView中添加选中状态变量,开启列表多选模式,并根据选中状态决定拖拽内容:
struct ContentView: View { @State private var items: [Item] = [ "First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh", "Eighth", "Ninth", "Tenth" ].map { Item(id: UUID(), name: $0) } @State private var selectedItems: Set<Item.ID> = [] @State private var editMode: EditMode = .inactive var body: some View { List(selection: $selectedItems) { ForEach(items) { item in VStack { Text(item.name) Text(item.id.uuidString) .font(.system(size: 10)) } // 选中多个时拖拽数组,否则拖拽单个元素 .draggable(selectedItems.isEmpty ? item : getSelectedItems()) // 选中元素高亮 .background(selectedItems.contains(item.id) ? Color.blue.opacity(0.2) : Color.clear) } .onMove(perform: move) .onInsert(of: [Item.singleItem, Item.multipleItems]) { index, providers in handleInsert(at: index, providers: providers) } } .navigationTitle("Draggable List") .toolbar { EditButton() } .environment(\.editMode, $editMode) } // 获取当前选中的元素数组 private func getSelectedItems() -> [Item] { selectedItems.compactMap { id in items.first { $0.id == id } } }
3. 处理插入逻辑(区分移动/复制)
实现插入处理方法,判断拖拽来源是否为当前列表:如果是则执行移动(删除原位置元素再插入),否则执行复制插入:
private func handleInsert(at index: Int, providers: [NSItemProvider]) { for provider in providers { // 优先处理多选数组 if provider.canLoadObject(ofClass: [Item].self) { provider.loadObject(ofClass: [Item].self) { loadedItems, error in guard let draggedItems = loadedItems as? [Item], error == nil else { return } DispatchQueue.main.async { // 找到拖拽元素在原数组中的索引 let sourceIndices = IndexSet(draggedItems.compactMap { item in items.firstIndex(where: { $0.id == item.id }) }) if !sourceIndices.isEmpty { // 同列表拖拽:删除原位置,插入到新位置 let filteredItems = draggedItems.filter { items.contains($0) } items.remove(atOffsets: sourceIndices) // 调整插入位置(原索引在插入位置前时需偏移) let adjustedIndex = index > sourceIndices.min()! ? index - sourceIndices.count : index items.insert(contentsOf: filteredItems, at: adjustedIndex) selectedItems.removeAll() } else { // 跨列表拖拽:直接插入 items.insert(contentsOf: draggedItems, at: index) } } } } else if provider.canLoadObject(ofClass: Item.self) { // 处理单个元素拖拽 provider.loadObject(ofClass: Item.self) { item, error in guard let draggedItem = item, error == nil else { return } DispatchQueue.main.async { items.insert(draggedItem, at: index) } } } } } // 保留编辑模式下的滑动移动逻辑 func move(from source: IndexSet, to destination: Int) { items.move(fromOffsets: source, toOffset: destination) } }
效果说明
进入编辑模式选中多个元素后,拖拽其中任意一个元素,即可将所有选中元素拖放到列表内任意位置:
- 同列表内拖拽会执行移动操作(原位置元素被删除)
- 跨列表拖拽会执行复制操作
- 选中元素会显示半透明蓝色背景,提升交互辨识度
内容的提问来源于stack exchange,提问作者Roman
相关产品推荐
相关产品推荐

