【问题标题】:How to properly group a list fetched from CoreData by date?如何按日期正确分组从 CoreData 获取的列表?
【发布时间】:2020-03-29 12:46:08
【问题描述】:

为了简单起见,假设我想创建一个简单的待办事项应用程序。我的 xcdatamodeld 中有一个实体 Todo,其属性为 idtitledate,以及以下 swiftui 视图(如图所示):

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>

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

  var body: some View {
    VStack {
      List {
        ForEach(todos, id: \.self) { todo in
          HStack {
            Text(todo.title ?? "")
            Text("\(todo.date ?? Date(), formatter: self.dateFormatter)")
          }
        }
      }
      Form {
        DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
          Text("Datum")
        }
      }
      Button(action: {
        let newTodo = Todo(context: self.moc)
        newTodo.title = String(Int.random(in: 0 ..< 100))
        newTodo.date = self.date
        newTodo.id = UUID()
        try? self.moc.save()
      }, label: {
        Text("Add new todo")
      })
    }
  }
}

待办事项在获取时按日期排序,并显示在如下列表中:

我想根据每个待办事项各自的日期对列表进行分组(样机):

据我了解,这可以与 init() 函数中的字典一起使用,但是我想不出任何远程有用的东西。有没有一种有效的数据分组方式?

【问题讨论】:

    标签: ios swift core-data swiftui


    【解决方案1】:

    您可以尝试以下方法,它应该适用于您的情况。

      @Environment(\.managedObjectContext) var moc
      @State private var date = Date()
      @FetchRequest(
        entity: Todo.entity(),
        sortDescriptors: [
          NSSortDescriptor(keyPath: \Todo.date, ascending: true)
        ]
      ) var todos: FetchedResults<Todo>
    
      var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        return formatter
      }
    
        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 {
        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)
          }
          Form {
            DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
              Text("Datum")
            }
          }
          Button(action: {
            let newTodo = Todo(context: self.moc)
            newTodo.title = String(Int.random(in: 0 ..< 100))
            newTodo.date = self.date
            newTodo.id = UUID()
            try? self.moc.save()
          }, label: {
            Text("Add new todo")
          })
        }
      }
    

    【讨论】:

    • 这看起来真不错!感谢您花时间回答。不幸的是,每当我从 ManagedObjectContext 添加或删除时,我都会遇到异常,我似乎无法弄清楚原因。你有什么想法? *** -[_UITableViewUpdateSupport _setupAnimationsForNewlyInsertedCells]、/BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore/UIKit-3900.12.16/UITableViewSupport.m:1311 中的断言失败 *** 由于未捕获的异常而终止应用程序' NSInternalInconsistencyException',原因:'尝试为单元格创建两个动画'
    • 您是否使用确切的代码运行?也许你错过了上面的一些东西。喜欢id(todos.count)
    • 您是对的,您的示例运行良好。我过于乐观,并通过列表元素上的 .sheet 为每个待办事项插入了一个详细信息页面。这似乎是罪魁祸首。我还没有找到解决方案,但会尝试并报告。
    • pbasdf 帮我解决了这个问题。所有的功劳归于他。将 }.values.map{$0} 更改为 }.values.sorted() { $0[0].date! &lt; $1[0].date! } 使其与 .sheet 一起使用并且错误消失了。
    • 这个解决方案最大的问题是字典没有排序,你在 NSFetchRequest 中所做的任何排序都会丢失:/
    【解决方案2】:

    iOS 15 更新

    SwiftUI 现在通过@SectionedFetchRequest 属性包装器在List 中内置了对分段获取请求的支持。这个包装器减少了对核心数据列表进行分组所需的样板数量。

    示例代码

    @Environment(\.managedObjectContext) var moc
    @State private var date = Date()
    @SectionedFetchRequest( // Here we use SectionedFetchRequest
      entity: Todo.entity(),
      sectionIdentifier: \.dateString // Add this line
      sortDescriptors: [
        SortDescriptor(\.date, order: .forward)
      ]
    ) var todos: SectionedFetchedResults<Todo>
    
    var body: some View {
        VStack {
          List {
            ForEach(todos) { (section: [Todo]) in
                Section(section[0].dateString!))) {
                    ForEach(section) { todo in
                        HStack {
                            Text(todo.title ?? "")
                            Text("\(todo.date ?? Date(), formatted: todo.dateFormatter)")
                        }
                    }
                }
            }.id(todos.count)
          }
          Form {
            DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {
              Text("Datum")
            }
          }
          Button(action: {
            let newTodo = Todo(context: self.moc)
            newTodo.title = String(Int.random(in: 0 ..< 100))
            newTodo.date = self.date
            newTodo.id = UUID()
            try? self.moc.save()
          }, label: {
            Text("Add new todo")
          })
        }
    

    Todo 类也可以重构为包含获取日期字符串的逻辑。作为奖励,我们还可以在Date 上使用.formatted beta 方法来生成相关的String

    struct Todo {
      
      ...
    
      var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        return formatter
      }()
    
      var dateString: String? {
        formatter.string(from: date)
      }
    }
    

    【讨论】:

      【解决方案3】:

      要将 Core Data 支持的 SwiftUI 列表划分为多个部分,您可以更改数据模型以支持分组。在这种特殊情况下,这可以通过将TodoSection 实体引入您的托管对象模型来实现。该实体将具有用于对部分进行排序的date 属性和一个唯一的name 字符串属性,该属性将用作部分ID,以及部分标题名称。可以通过对托管对象使用 Core Data 唯一约束来强制执行独特的质量。每个部分中的待办事项都可以建模为与Todo 实体的对多关系。

      保存新的Todo 对象时,您必须使用查找或创建 模式来确定您是否已经有一个部分在商店中,或者您必须创建一个新的部分。

          let sectionName = dateFormatter.string(from: date)
          let sectionFetch: NSFetchRequest<TodoSection> = TodoSection.fetchRequest()
          sectionFetch.predicate = NSPredicate(format: "%K == %@", #keyPath(TodoSection.name), sectionName)
          
          let results = try! moc.fetch(sectionFetch)
          
          if results.isEmpty {
              // Section not found, create new section.
              let newSection = TodoSection(context: moc)
              newSection.name = sectionName
              newSection.date = date
              newSection.addToTodos(newTodo)
          } else {
              // Section found, use it.
              let existingSection = results.first!
              existingSection.addToTodos(newTodo)
          }
      

      要显示您的部分和随附的待办事项,嵌套的 ForEach 视图可以在两者之间使用 Section。 Core Data 将NSSet? 用于to many 关系,因此您必须使用数组代理并将TodoComparable 一致才能使用SwiftUI。

          extension TodoSection {
              var todosArrayProxy: [Todo] {
                  (todos as? Set<Todo> ?? []).sorted()
              }
          }
          
          extension Todo: Comparable {
              public static func < (lhs: Todo, rhs: Todo) -> Bool {
                  lhs.title! < rhs.title!
              }
          }
      

      如果您需要删除某个 todo,请记住,section 中最后删除的 todo 也应该删除整个 section 对象。

      我尝试在Dictionary 上使用init(grouping:by:),正如这里所建议的那样,在我的情况下,它会导致锯齿状的动画,这可能表明我们走错了方向。我猜当我们删除单个项目时,必须重新编译整个项目列表。此外,随着我​​们数据集的增长,将分组嵌入到数据模型中会更加高效且面向未来。

      如果您需要任何进一步的参考,我已经提供了a sample project

      【讨论】:

        猜你喜欢
        • 2014-02-12
        • 2023-01-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-26
        • 1970-01-01
        • 2022-01-23
        • 1970-01-01
        相关资源
        最近更新 更多