【问题标题】:How to group core data items by date in SwiftUI?如何在 SwiftUI 中按日期对核心数据项进行分组?
【发布时间】:2026-01-22 21:45:01
【问题描述】:

我在 iOS 应用中拥有的是:

TO DO ITEMS

To do item 3
24/03/2020
------------
To do item 2
24/03/2020
------------
To do item 1
23/03/2020
------------

我想要的是:

TO DO ITEMS


24/03

To do item 3
24/03/2020
------------
To do item 2
24/03/2020
------------


23/03

To do item 1
23/03/2020
------------

================

我目前所拥有的:

我正在使用 Core Data 并且有 1 个实体:Todo。模块:当前产品模块。 Codegen:类定义。 该实体有2个属性:标题(字符串)、日期(日期)

ContentView.swift 显示列表。

import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @State private var date = Date()
    @FetchRequest(
        entity: Todo.entity(),
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Todo.date, ascending: true)
        ]
    ) var todos: FetchedResults<Todo>

    @State private var show_modal: Bool = false

    var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        return formatter
    }

    // func to group items per date. Seemed to work at first, but crashes the app if I try to add new items using .sheet
    func update(_ result : FetchedResults<Todo>)-> [[Todo]]{
        return  Dictionary(grouping: result){ (element : Todo)  in
            dateFormatter.string(from: element.date!)
        }.values.map{$0}
    }

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(update(todos), id: \.self) { (section: [Todo]) in
                        Section(header: Text( self.dateFormatter.string(from: section[0].date!))) {
                            ForEach(section, id: \.self) { todo in
                                HStack {
                                    Text(todo.title ?? "")
                                    Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
                                }
                            }
                        }
                    }.id(todos.count)

                    // With this loop there is no crash, but it doesn't group items
                    //ForEach(Array(todos.enumerated()), id: \.element) {(i, todo) in
                    //    HStack {
                    //        Text(todo.title ?? "")
                    //        Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
                    //    }
                    //}

                }
            }
            .navigationBarTitle(Text("To do items"))
            .navigationBarItems(
                trailing:
                Button(action: {
                    self.show_modal = true
                }) {
                    Text("Add")
                }.sheet(isPresented: self.$show_modal) {
                    TodoAddView().environment(\.managedObjectContext, self.moc)
                }
            )
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        return ContentView().environment(\.managedObjectContext, context)
    }
}

TodoAddView.swift 在此视图中,我添加了新项目。

import SwiftUI

struct TodoAddView: View {

    @Environment(\.presentationMode) var presentationMode
    @Environment(\.managedObjectContext) var moc

    static let dateFormat: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter
    }()

    @State private var showDatePicker = false
    @State private var title = ""
    @State private var date : Date = Date()

    var body: some View {
        NavigationView {

            VStack {
                HStack {
                    Button(action: {
                        self.showDatePicker.toggle()
                    }) {
                        Text("\(date, formatter: Self.dateFormat)")
                    }

                    Spacer()
                }

                if self.showDatePicker {
                    DatePicker(
                        selection: $date,
                        displayedComponents: .date,
                        label: { Text("Date") }
                    )
                        .labelsHidden()
                }

                TextField("title", text: $title)

                Spacer()

            }
            .padding()
            .navigationBarTitle(Text("Add to do item"))
            .navigationBarItems(
                leading:
                Button(action: {
                    self.presentationMode.wrappedValue.dismiss()
                }) {
                    Text("Cancel")
                },

                trailing:
                Button(action: {

                    let todo = Todo(context: self.moc)
                    todo.date = self.date
                    todo.title = self.title

                    do {
                        try self.moc.save()
                    }catch{
                        print(error)
                    }

                    self.presentationMode.wrappedValue.dismiss()
                }) {
                    Text("Done")
                }
            )
        }
    }
}

struct TodoAddView_Previews: PreviewProvider {
    static var previews: some View {
        TodoAddView()
    }
}

我试过这个: 我已经搜索了一些例子。一个看起来不错:How to properly group a list fetched from CoreData by date? 我从那里使用了更新功能和 ForEach,但它不适用于 SwiftUI 中的 .sheet 。当我打开 .sheet(点击添加后)时,应用程序崩溃并出现错误:

线程 1:异常:“尝试为单元格创建两个动画”

如何解决?还是有另一种按日期对核心数据进行分组的方法?有人告诉我应该将分组添加到我的数据模型中。稍后在 UI 中显示。我不知道从哪里开始。

另一个猜测是我也许可以编辑我的@FetchRequest 代码以在那里添加分组。但我正在寻找几天没有运气的解决方案。 我知道 Core Data 中有一个 setPropertiesToGroupBy,但我不知道它是否以及如何与 @FetchRequest 和 SwiftUI 一起使用。

另一个猜测: 是否可以使用 Dictionary(grouping: attributeName) 根据属性在 SwiftUI 中对 CoreData Entity 实例进行分组? 分组数组看起来很简单:https://www.hackingwithswift.com/example-code/language/how-to-group-arrays-using-dictionaries,但我不知道它是否以及如何与 Core Data 和 @FetchRequest 一起使用。

【问题讨论】:

  • 如果你想分组,那么你需要使用我认为的列表,否则你将不得不手动进行。
  • 谢谢。我认为 List 是个好主意。但是如何从核心数据中获取项目,对它们进行分组并在每个组中按 id 排序?我被告知要在数据模型中进行分组。但这我真的不知道如何开始:/
  • 在您的示例中,您按日期分组,如果这是您想要的,那么链接的答案应该会有所帮助。您还在问题中提到了类别,如果这是您想要的,那么您应该为它添加一个具有一对多关系的核心数据模型的实体,以便项目可以有一个类别。
  • 这个链接的答案不适用于 SwiftUI .sheet。它使应用程序崩溃。现在让我们忘记预定义的类别。我想要的最低要求是按日期分组
  • 老实说,我不明白 .sheet 与它有什么关系,那不只是一种窗口吗?你不能在工作表中有一个列表还是有什么问题?链接的答案是使用 SwiftUI,因此您的声明一定是错误的。

标签: ios swift core-data swiftui categories


【解决方案1】:

我自己刚刚进入 SwiftUI,所以这可能是一个误解,但我认为问题在于 update 函数不稳定,因为它不能保证以相同的顺序返回组每一次。因此,当添加新项目时,SwiftUI 会感到困惑。我发现通过专门对数组进行排序可以避免错误:

func update(_ result : FetchedResults<Todo>)-> [[Todo]]{
    return  Dictionary(grouping: result){ (element : Todo)  in
        dateFormatter.string(from: element.date!)
    }.values.sorted() { $0[0].date! < $1[0].date! }
}

【讨论】:

    【解决方案2】:

    我对编程也很陌生,以下解决方案可能不太优雅,但我很自豪自己能解决这个问题!

    我在名为 lastOfDay 的对象中添加了一个 bool,它触发了该对象上日期的文本视图:

    ForEach(allResults) { result in
                            VStack(spacing: 0) {
                                if currentMethod == .byDate {
                                    if result.lastOfDay {
                                        Text("\(result.date, formatter: dateFormatter)")
                                    }
                                }
                                ListView(result: result)
                            }
                        }
    

    然后我有一个 onAppear 函数,它将我获取的结果复制到一个单独的非 CoreData 数组中,按日期组织它们并检查下一个结果的日期是否与当前对象的日期不同 - 并翻转必要的布尔值。我希望通过某些版本的 .map 来实现这一点,但我认为有必要考虑列表为空或只有一个项目的情况。

     if allResults.count != 0 {
                    
                    if allResults.count == 1 {
                        allResults[0].lastOfDay = true
                    }
                    
                    for i in 0..<(allResults.count-1) {
                        
                        if allResults.count > 1 {
                            
                            allResults[0].lastOfDay = true
                            
                            if allResults[i].date.hasSame(.day, as: allResults[i+1].date)  {
                                allResults[i+1].lastOfDay = false
                            } else {
                                allResults[i+1].lastOfDay = true
                            }
                        }
                    }
                }
    

    我在in this answer. 上使用的 hasSame 日期扩展方法我不知道如果您希望让用户删除批次,这种方法的效果如何,但它对我来说非常有效,因为我只想实现单数删除或删除所有对象(但是,由于每次发生此类更改时我都会触发过滤过程 - 使用更大的数据集可能会变得昂贵)。

    【讨论】:

      【解决方案3】:

      将分组嵌入到托管对象模型中是可行的方法,因为它会更加健壮,并且可以很好地处理大型数据集。我已经向an answera sample project 提供了如何实现它。

      当我们在 Dictionary 上使用 init(grouping:by:) 时,我们可能会在每次执行任何列表操作(例如插入或删除)时重新编译整个列表,并由执行分组的字典支持,这不是高效的,并且,在我的情况下,会导致锯齿状的动画。从存储中获取以一种方式排序的实体,然后再次在本地进行排序以将它们分成多个部分,这在性能方面没有意义。

      据我所知,无法使用 fetch 请求进行分组,因为 propertiesToGroupBy 是使用 SQL GROUP BY 查询的核心数据接口,旨在与聚合函数(例如 min、max、sum)一起使用而不是将数据集划分为部分。

      【讨论】: