For the sake of simplicity lets assume I want to create a simple todo app. I have an entity Todo in my xcdatamodeld with the properties id
, title
and date
, and the following swiftui view (example pictured):
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")
})
}
}
}
The todos are sorted by date upon fetching, and are displayed in a list like this:
I want to group the list based on each todos respective date as such (mockup):
From my understanding this could work with Dictionaries in the init()
function, however I couldn't come up with anything remotely useful. Is there an efficient way to group data?
You may try the following, It should work in your situation.
@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")
})
}
}
SwiftUI
now has built-in support for Sectioned Fetch Requests in a List
via the @SectionedFetchRequest
property wrapper. This wrapper reduces the amount of boilerplate required to group Core Data lists.
@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")
})
}
The Todo
class can also be refactored to contain the logic for getting the date string. As a bonus, we can also use the .formatted
beta method on Date
to produce the relevant String
.
struct Todo {
...
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}()
var dateString: String? {
formatter.string(from: date)
}
}
To divide SwiftUI List backed by Core Data into sections, you can change your data model to support grouping. In this particular case, this can be achieved by introducing TodoSection
entity to your managed object model. The entity would have a date
attribute for sorting sections and a unique name
string attribute that would serve as a section id, as well as a section header name. The unique quality can be enforced by using Core Data unique constraints on your managed object. Todos in each section can be modeled as a to many relationship to your Todo
entity.
When saving a new Todo
object, you would have to use Find or Create pattern to find out whether you already have a section in store or you would have to create a new one.
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)
}
To display your sections and accompanying todos nested ForEach
views can be used with Section
in between. Core Data uses NSSet?
for to many relationships so you would have to use an array proxy and conform Todo
to Comparable
for everything to work with 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!
}
}
If you need to delete a certain todo, bear in mind that the last removed todo in section should also delete the entire section object.
I tried using init(grouping:by:)
on Dictionary
, as it has been suggested here, and, in my case, it causes jaggy animations, which are probably the sign that we are going in the wrong direction. I’m guessing the whole list of items has to be recompiled when we delete a single item. Furthermore, embedding grouping into a data model would be more performant and future-proof as our data set grows.
I have provided a sample project if you need any further reference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With