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

SwiftUI中分组列表过滤项的重排序问题解决

问题原因

你遇到的核心问题是过滤/分组后的列表索引与原始数据数组的索引不匹配:当前ForEach遍历的是过滤后的子集,但onMove/onDelete调用的方法直接操作原始items数组的索引,移动/删除时传递的是过滤列表的索引,直接用这些索引操作原始数组会导致数据未正确更新,刷新后列表又回到原顺序。

解决方案

要实现按时段分组的可重排列表,需要将过滤/分组后的索引映射回原始数组的索引,确保所有操作最终作用于原始数据源。以下是具体实现步骤:

1. 完善视图模型(ListViewModel)

添加处理过滤/分组索引映射的方法,确保移动/删除操作能正确更新原始数组:

class ListViewModel: ObservableObject {
    @Published var items: [ItemModel] = [] {
        didSet {
            saveItems()
        }
    }
    
    // 按时段分组的数据源
    var groupedItems: [PartOfDay: [ItemModel]] {
        Dictionary(grouping: items, by: { $0.partOfDay })
    }
    
    let itemsKey: String = "items_list"

    init() {
        getItems()
    }

    func getItems() {
        guard
            let data = UserDefaults.standard.data(forKey: itemsKey),
            let savedItems = try? JSONDecoder().decode([ItemModel].self, from: data)
        else { return }
        self.items = savedItems
    }

    func deleteItem(indexSet: IndexSet) {
        items.remove(atOffsets: indexSet)
    }

    func moveItem(from: IndexSet, to: Int) {
        items.move(fromOffsets: from, toOffset: to)
    }

    func addItem(title: String, partOfDay: PartOfDay) {
        let newItem = ItemModel(title: title, partOfDay: partOfDay, isCompleted: false)
        items.append(newItem)
    }

    func updateItem(item: ItemModel) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index] = item.updateCompletion()
        }
    }

    func saveItems() {
        if let encodedData = try? JSONEncoder().encode(items) {
            UserDefaults.standard.setValue(encodedData, forKey: itemsKey)
        }
    }
    
    // MARK: - 新增:处理过滤/分组列表的移动
    func moveFilteredItem(from filteredIndices: IndexSet, to filteredOffset: Int, filter: (ItemModel) -> Bool) {
        let filteredItemsArray = items.filter(filter)
        guard !filteredItemsArray.isEmpty else { return }
        
        // 1. 将过滤列表的索引映射为原始数组的索引
        let originalFromIndices = IndexSet(filteredIndices.map { idx in
            items.firstIndex(where: { $0.id == filteredItemsArray[idx].id })!
        })
        
        // 2. 计算目标位置在原始数组中的索引
        let originalToIndex: Int
        if filteredOffset >= filteredItemsArray.count {
            // 移动到分组末尾:取分组最后一个元素的下一个位置
            let lastFilteredItem = filteredItemsArray.last!
            originalToIndex = (items.firstIndex(where: { $0.id == lastFilteredItem.id }) ?? items.count) + 1
        } else {
            // 移动到分组中间:取目标位置元素的原始索引
            let targetItem = filteredItemsArray[filteredOffset]
            originalToIndex = items.firstIndex(where: { $0.id == targetItem.id }) ?? items.count
        }
        
        // 3. 执行原始数组的移动操作
        items.move(fromOffsets: originalFromIndices, toOffset: originalToIndex)
    }
    
    // MARK: - 新增:获取指定时段的任务列表
    func items(for part: PartOfDay) -> [ItemModel] {
        groupedItems[part] ?? []
    }
}

2. 修改视图(TaskListView)

实现按早/中/晚/夜分组的列表,每组单独处理移动和删除:

struct TaskListView: View {
    @EnvironmentObject var listViewModel: ListViewModel

    var body: some View {
        ZStack {
            if listViewModel.items.isEmpty {
                Text("No items")
            } else {
                List {
                    // 遍历所有时段分组
                    ForEach(PartOfDay.allCases, id: \.self) { part in
                        Section(header: Text(part.rawValue.capitalized)) {
                            ForEach(listViewModel.items(for: part)) { item in
                                ListRowView(item: item)
                                    .onTapGesture {
                                        withAnimation(.linear){
                                            listViewModel.updateItem(item: item)
                                        }
                                    }
                            }
                            // 分组内删除
                            .onDelete { indexSet in
                                let filteredItems = listViewModel.items(for: part)
                                let originalIndices = IndexSet(indexSet.map { idx in
                                    listViewModel.items.firstIndex(where: { $0.id == filteredItems[idx].id })!
                                })
                                listViewModel.deleteItem(indexSet: originalIndices)
                            }
                            // 分组内移动
                            .onMove { from, to in
                                listViewModel.moveFilteredItem(from: from, to: to) { $0.partOfDay == part }
                            }
                        }
                    }
                }
                .listStyle(.sidebar)
            }
        }
        .navigationBarItems(
            leading: EditButton(),
            trailing: NavigationLink("Add", destination: AddView())
        )
    }
}

3. 确保ItemModel与PartOfDay定义正确

// 时段枚举
enum PartOfDay: String, Codable, CaseIterable {
    case morning = "morning"
    case afternoon = "afternoon"
    case evening = "evening"
    case night = "night"
}

// 任务模型
struct ItemModel: Identifiable, Codable {
    let id: UUID
    let title: String
    let partOfDay: PartOfDay
    var isCompleted: Bool
    
    init(id: UUID = UUID(), title: String, partOfDay: PartOfDay, isCompleted: Bool) {
        self.id = id
        self.title = title
        self.partOfDay = partOfDay
        self.isCompleted = isCompleted
    }
    
    func updateCompletion() -> ItemModel {
        ItemModel(id: id, title: title, partOfDay: partOfDay, isCompleted: !isCompleted)
    }
}
关键要点
  • 所有列表操作(移动/删除)必须通过元素的id映射回原始数组的索引,不能直接使用过滤/分组后的索引
  • 分组场景下,每组单独处理操作逻辑,确保只修改对应分组内的元素顺序
  • 移动目标位置需要区分分组中间和末尾两种情况,避免索引计算错误

内容的提问来源于stack exchange,提问作者Elena Rubilova

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:15:00