You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 22:07:50