如何正确地按日期对从CoreData获取的列表进行分组?

18

为了简单起见,假设我想创建一个简单的待办事项应用。我在我的xcdatamodeld中有一个名为Todo的实体,该实体具有属性 id , title 和 date ,以及以下SwiftUI视图(示例如图所示):

为了简化操作,我们假设希望创建一个简单的待办事项应用程序。 我们的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")
      })
    }
  }
}
待办事项将在获取时按日期排序,并以此列表形式显示:

What I got

我想根据每个待办事项的日期分组,如下所示(模拟):

What I want

据我了解,这可以通过`init()`函数中的字典实现,但我无法想出任何有用的方法。是否有一种有效的数据分组方法?
3个回答

19

iOS 15更新

SwiftUI现在通过@SectionedFetchRequest属性包装器为List提供分组获取请求的内置支持。该包装器减少了分组Core Data列表所需的样板代码。

示例代码

@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: SectionedFetchResults<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测试版方法来生成相关的String

struct Todo {
  
  ...

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

  var dateString: String? {
    formatter.string(from: date)
  }
}

16

您可以尝试以下方法,在您的情况下应该有效。

  @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")
      })
    }
  }

1
这看起来非常不错!感谢您抽出时间回答。不幸的是,每当我添加或删除ManagedObjectContext时,我都会遇到异常情况,而且似乎无法找出原因。您有什么想法吗?*** Assertion failure in -[_UITableViewUpdateSupport _setupAnimationsForNewlyInsertedCells], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore/UIKit-3900.12.16/UITableViewSupport.m:1311 *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempt to create two animations for cell' - fasoh
你是否使用了完全相同的代码?也许你在上面漏掉了一些东西,例如 id(todos.count) - E.Coms
你说得对,你的例子很好用。我过于乐观了,通过在List元素上使用.sheet为每个待办事项插入了一个详细页面。这似乎是罪魁祸首。我还没有找到解决方案,但会尝试并报告结果。 - fasoh
4
pbasdf帮助我解决了这个问题。所有的功劳归功于他。将}.values.map{$0}更改为}.values.sorted() { $0[0].date! < $1[0].date! }将使其与.sheet一起工作,错误也会消失。 - mallow
1
这个解决方案最大的问题是字典没有排序,无论你在NSFetchRequest中做了什么排序都会丢失 :/ - iSebbeYT
显示剩余5条评论

5
为了将由Core Data支持的SwiftUI列表分成多个部分,你需要更改你的数据模型来支持分组。在这种情况下,可以通过向托管对象模型引入TodoSection实体来实现。该实体将具有一个用于排序的date属性和一个唯一的name字符串属性,它将作为部分id以及部分标题名称。可以使用Core Data唯一约束来强制执行唯一性。每个部分中的待办事项可以建模为与您的Todo实体的to many关系。
在保存新的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)
    }

为了展示你的区块和相应的待办事项,可以使用具有Section之间的ForEach视图。Core Data对于一对多关系使用NSSet?,因此您需要使用数组代理并符合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!
        }
    }

如果需要删除某个待办事项,请记住,部分中最后一个已删除的待办事项也应删除整个部分对象。

我尝试在Dictionary上使用init(grouping:by:),因为在这里提出了建议,在我的情况下,它会导致不流畅的动画,这可能是我们朝错误方向前进的迹象。我猜测当我们删除单个项目时,整个项目列表都必须重新编译。此外,将分组嵌入数据模型将更具性能和未来性,随着我们的数据集增长。

如果您需要任何进一步的参考,我提供了样本项目


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接