What I have in my iOS app is:
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. Module: Current Product Module. Codegen: Class Definition. This entity has 2 attributes: title (String), date (Date).
ContentView.swift Displays the list.
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.
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? and I have used the update function and ForEach from there, but it doesn't work with .sheet in SwiftUI. When I open the .sheet (after tapping Add) the app crashes with an error:
Thread 1: Exception: "Attempt to create two animations for cell"
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. I don't know where to start.
Another guess is that I maybe could edit my @FetchRequest code to add grouping there. 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.
Another guess: Is it possible to use Dictionary(grouping: attributeName) to group CoreData Entity instances in SwiftUI based on their attributes? 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.
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 consequently gets confused when new items are added. 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! }
}