简体   繁体   English

如何在 SwiftUI 中按日期对核心数据项进行分组?

[英]How to group core data items by date in SwiftUI?

What I have in my iOS app is:我在我的 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
------------

What I would like to have is:我想要的是:

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
------------

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

What I have so far:到目前为止我所拥有的:

I am using Core Data and have 1 Entity: Todo .我正在使用Core Data并且有1 个 Entity: Todo Module: Current Product Module.模块:当前产品模块。 Codegen: Class Definition. Codegen:类定义。 This entity has 2 attributes: title (String), date (Date) .该实体有2 个属性:标题 (String)、日期 (Date)

ContentView.swift Displays the list. 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 In this view I add new item. 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()
    }
}

I have tried this: I have searched for some examples.我试过这个:我已经搜索了一些例子。 One looked good: How to properly group a list fetched from CoreData by date?一个看起来不错: How to properly group a list fetched from CoreData by date? and I have used the update function and ForEach from there, but it doesn't work with.sheet in SwiftUI.我从那里使用了更新功能和 ForEach,但它不适用于 SwiftUI 中的 .sheet。 When I open the.sheet (after tapping Add) the app crashes with an error:当我打开 .sheet(点击添加后)时,应用程序崩溃并出现错误:

Thread 1: Exception: "Attempt to create two animations for cell"线程 1:异常:“尝试为单元格创建两个动画”

How to fix it?如何解决? Or is there another way of grouping core data by date?还是有另一种按日期对核心数据进行分组的方法? I have been told that I should add grouping to my data model.有人告诉我应该将分组添加到我的数据模型中。 And just show it later in UI.稍后在 UI 中显示它。 I don't know where to start.我不知道从哪里开始。

Another guess is that I maybe could edit my @FetchRequest code to add grouping there.另一个猜测是我也许可以编辑我的@FetchRequest 代码以在其中添加分组。 But I am searching for a solution few days without luck.但是我正在寻找几天没有运气的解决方案。 I know there is a setPropertiesToGroupBy in Core Data, but I don't know if and how it works with @FetchRequest and SwiftUI.我知道核心数据中有一个 setPropertiesToGroupBy,但我不知道它是否以及如何与 @FetchRequest 和 SwiftUI 一起工作。

Another guess: Is it possible to use Dictionary(grouping: attributeName) to group CoreData Entity instances in SwiftUI based on their attributes?另一个猜测:是否可以使用 Dictionary(grouping: attributeName) 根据属性对 SwiftUI 中的 CoreData 实体实例进行分组? Grouping arrays looks so easy: https://www.hackingwithswift.com/example-code/language/how-to-group-arrays-using-dictionaries , but I don't know if and how it works with Core Data and @FetchRequest.数组分组看起来很简单: https ://www.hackingwithswift.com/example-code/language/how-to-group-arrays-using-dictionaries ,但我不知道它是否以及如何与 Core Data 和@一起工作获取请求。

I'm just getting into SwiftUI myself, so this might be a misunderstanding, but I think the issue is that the update function is unstable, in the sense that it does not guarantee to return the groups in the same order each time.我自己刚刚进入 SwiftUI,所以这可能是一个误解,但我认为问题在于update功能不稳定,因为它不能保证每次都以相同的顺序返回组。 SwiftUI consequently gets confused when new items are added.因此,当添加新项目时,SwiftUI 会感到困惑。 I found that the errors were avoided by specifically sorting the array:我发现通过对数组进行专门排序可以避免错误:

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! }
}

I'm also quite new to programming and the following solution might be a little less than elegant but ngl I'm quite proud to figure it out myself!我对编程也很陌生,下面的解决方案可能不太优雅,但是我很自豪能够自己解决这个问题!

I added a bool to my object named lastOfDay that triggers a textview of the date on that object:我向名为 lastOfDay 的对象添加了一个布尔值,它会触发该对象上日期的文本视图:

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

Then I have an onAppear function that copies my fetched results to a separate, non-CoreData array, organizing them by date and checking whether the next result's day is different from the current object's day - and flipping the necessary bools.然后我有一个 onAppear 函数,它将我获取的结果复制到一个单独的非 CoreData 数组,按日期组织它们并检查下一个结果的日期是否与当前对象的日期不同 - 并翻转必要的布尔值。 I hoped to achieve this through some version of .map but figured that it was necessary to account for situations when the list was empty or only had a single item.我希望通过某些版本的 .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
                        }
                    }
                }
            }

The hasSame date extension method I picked up on in this answer.在这个答案中选择的 hasSame 日期扩展方法 I don't know how well this approach will work if you desire to let the user delete batches but it works perfectly for me because I only want to implement either singular deletes or delete all objects (however since I trigger the filtering process every time such a change happens - usage might get expensive w bigger data sets).如果您希望让用户删除批次,我不知道这种方法的效果如何,但它对我来说非常有效,因为我只想实现单个删除或删除所有对象(但是因为我每次都触发过滤过程)发生了变化-使用更大的数据集可能会变得昂贵)。

Embedding grouping into a managed object model is the way to go because it would be more robust and will work well with large data sets.将分组嵌入托管对象模型是一种可行的方法,因为它会更加健壮,并且可以很好地处理大型数据集。 I have provided an answer with a sample project on how to implement it.我已经提供 一个关于如何实现它 的示例项目的答案

When we are using init(grouping:by:) on Dictionary , we are likely recompiling the whole list, backed by a dictionary that does the grouping, every time we perform any list manipulation such as insertion or deletion, which is not performant and, in my case, causes jaggy animations.当我们在Dictionary上使用init(grouping:by:)时,我们可能会重新编译整个列表,并由一个进行分组的字典支持,每次我们执行任何列表操作(例如插入或删除)时,这都是无效的,并且,就我而言,会导致锯齿状动画。 It doesn't make sense performance-wise to fetch entities from the store sorted one way and then do the sorting locally again to divide them into sections.从以一种方式排序的存储中获取实体,然后再次在本地进行排序以将它们划分为多个部分,在性能方面没有意义。

Grouping with fetch request is not possible, as far as I know, because propertiesToGroupBy is the Core Data interface for using SQL GROUP BY query and is meant to be used with aggregate functions (eg min, max, sum) and not to divide data sets into sections.据我所知,使用 fetch 请求分组是不可能的,因为propertiesToGroupBy是使用 SQL GROUP BY查询的核心数据接口,旨在与聚合函数(例如 min、max、sum)一起使用而不是划分数据集成部分。

Looks like @SectionedFetchRequest property wrapper exists just for such task.看起来@SectionedFetchRequest属性包装器只是为了这样的任务而存在。 Below is an example made from boilerplate CoreData project.下面是从样板 CoreData 项目制作的示例。 The key is that you have to mark a var you're sectioning by as @objc.关键是您必须将要分段的 var 标记为 @objc。

extension Todo {
    @objc
    var sect: String { date?.formatted(date: .abbreviated, time: .omitted) ?? "Undefined" }
}

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @SectionedFetchRequest(
        sectionIdentifier: \Todo.sect,
        sortDescriptors: [NSSortDescriptor(keyPath: \Todo.date, ascending: true)],
        animation: .default)
    private var items

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { section in
                    Section(section.id) {
                        ForEach(section) { item in
                            NavigationLink {
                                Text("Item at \(item.date!, format: .dateTime.year().month().hour().minute().second())")
                            } label: {
                                Text(item.date!, formatter: itemFormatter)
                            }
                        }
                    }
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Todo(context: viewContext)
            newItem.date = Date()

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM