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

SwiftUI多视图间多项目拖拽及分区内重排方案咨询

SwiftUI实现跨视图多项目拖拽+分区内重排方案

核心思路

  1. 维护跨视图共享的选中项目集合,标记需要拖拽的项
  2. 自定义拖拽数据结构,将所有选中项打包后通过单个NSItemProvider传递
  3. 实现自定义DropDelegate处理跨视图接收与分区内重排逻辑
  4. 结合LazyVStack和onMove保留分区内的重排能力

具体实现步骤

1. 定义数据模型与选中状态管理

先实现可序列化的数据模型,以及跨视图共享的选中状态管理器:

import SwiftUI

struct Item: Identifiable, Codable {
    let id: UUID
    let title: String
    let sectionId: String // 标记所属分区
}

class DragSelectionManager: ObservableObject {
    @Published var selectedItemIds: Set<UUID> = []
    
    func toggleSelection(for item: Item) {
        selectedItemIds.contains(item.id) ? selectedItemIds.remove(item.id) : selectedItemIds.insert(item.id)
    }
    
    func clearSelection() {
        selectedItemIds.removeAll()
    }
}

2. 实现可多选拖拽的项目视图

在项目视图中绑定选中状态,拖拽时打包所有选中项的数据:

struct DraggableItemView: View {
    let item: Item
    @ObservedObject var selectionManager: DragSelectionManager
    @Binding var items: [Item]
    
    var body: some View {
        HStack {
            Text(item.title)
                .padding()
                .background(selectionManager.selectedItemIds.contains(item.id) ? Color.blue.opacity(0.3) : Color.clear)
                .cornerRadius(8)
            
            Spacer()
            
            if selectionManager.selectedItemIds.contains(item.id) {
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.blue)
            }
        }
        .onTapGesture {
            selectionManager.toggleSelection(for: item)
        }
        .onDrag {
            let selectedItems = items.filter { selectionManager.selectedItemIds.contains($0.id) }
            guard let data = try? JSONEncoder().encode(selectedItems) else {
                return NSItemProvider()
            }
            return NSItemProvider(object: data as NSData)
        }
        .onDrop(of: [.data], delegate: ItemDropDelegate(
            item: item,
            items: $items,
            selectionManager: selectionManager
        ))
    }
}

3. 自定义DropDelegate处理接收逻辑

实现DropDelegate来处理跨视图拖拽接收和分区内重排:

class ItemDropDelegate: DropDelegate {
    let item: Item
    @Binding var items: [Item]
    let selectionManager: DragSelectionManager
    
    init(item: Item, items: Binding<[Item]>, selectionManager: DragSelectionManager) {
        self.item = item
        self._items = items
        self.selectionManager = selectionManager
    }
    
    func performDrop(info: DropInfo) -> Bool {
        guard let provider = info.itemProviders(for: [.data]).first else { return false }
        
        provider.loadObject(ofClass: NSData.self) { data, _ in
            guard let data = data as? Data,
                  let droppedItems = try? JSONDecoder().decode([Item].self, from: data) else {
                return
            }
            
            DispatchQueue.main.async {
                // 移除当前分区内的选中项
                let filteredItems = self.items.filter { !self.selectionManager.selectedItemIds.contains($0.id) }
                let insertIndex = self.items.firstIndex(of: self.item) ?? filteredItems.count
                
                // 插入拖拽过来的项
                self.items = Array(filteredItems[0..<insertIndex]) + droppedItems + Array(filteredItems[insertIndex...])
                
                self.selectionManager.clearSelection()
            }
        }
        return true
    }
    
    func proposal(for info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
}

4. 构建分区视图与跨视图布局

用LazyVStack构建分区,让两个视图共享同一个选中状态管理器:

struct ContentView: View {
    @StateObject private var selectionManager = DragSelectionManager()
    @State private var sectionA: [Item] = [
        Item(id: UUID(), title: "A1", sectionId: "A"),
        Item(id: UUID(), title: "A2", sectionId: "A")
    ]
    @State private var sectionB: [Item] = [
        Item(id: UUID(), title: "B1", sectionId: "B"),
        Item(id: UUID(), title: "B2", sectionId: "B")
    ]
    @Environment(\.editMode) private var editMode
    
    var body: some View {
        VStack(spacing: 20) {
            Button(editMode?.wrappedValue == .active ? "完成" : "编辑") {
                withAnimation {
                    editMode?.wrappedValue = editMode?.wrappedValue == .active ? .inactive : .active
                }
            }
            
            SectionView(title: "分区A", items: $sectionA, selectionManager: selectionManager)
            SectionView(title: "分区B", items: $sectionB, selectionManager: selectionManager)
        }
        .padding()
        .environment(\.editMode, $editMode)
    }
}

struct SectionView: View {
    let title: String
    @Binding var items: [Item]
    @ObservedObject var selectionManager: DragSelectionManager
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.headline)
                .padding(.bottom, 8)
            
            LazyVStack(spacing: 8) {
                ForEach(items) { item in
                    DraggableItemView(item: item, selectionManager: selectionManager, items: $items)
                }
                .onMove(perform: moveItems)
            }
            .padding(.leading)
        }
    }
    
    func moveItems(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}

关键注意点

  • 跨视图拖拽时,若两个分区数据源独立,需在DropDelegate中同步更新原分区的数组(可通过全局数据源或通知实现)
  • 选中状态的视觉反馈需清晰,让用户明确知晓拖拽范围
  • 可通过preview修饰符自定义拖拽预览视图,优化用户体验
  • LazyVStack适合长列表场景,能保证拖拽时的性能

内容的提问来源于stack exchange,提问作者Anastasia Veremiichyk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 22:05:42