Reputation: 2856
My app crashes and I am getting a following error when I try to sort items by date in a ForEach loop:
2020-03-24 16:55:13.830146+0700 list-dates[60035:2135088] *** Assertion failure in -[_UITableViewUpdateSupport _setupAnimationsForNewlyInsertedCells], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3901.4.2/UITableViewSupport.m:1311 (lldb)
on line:
class AppDelegate: UIResponder, UIApplicationDelegate {
of AppDelegate.swift
At first my app loads in simulator, but after I tap Add button to open modal window and add new item, the app crashes immediately with the error.
I think that there is a problem in the func update or in the ForEach loop itself. I have marked in the code which alternative loop works for me. Sadly, this alternative doesn't group items by dates. And this is a feature I am trying to add in my app.
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
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
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 am using CoreData. In this example there is 1 entity named Todo with 2 attributes: date (Date), title (String).
I would be grateful if someone can help me with the bug. Or an alternative to grouping items could work too :)
Upvotes: 1
Views: 2105
Reputation: 17544
For inspiration, how to use your model, here is simplified example
import SwiftUI // it imports all the necessary stuff ...
We need some data structure for our task conforming to Identifiable (this will help SwiftUI to identify each dynamically generated ToDoView)
struct ToDo: Identifiable {
let id = UUID()
let date: Date
let task: String
var done = false
}
Simple model with all basic functionality could be defined as
class ToDoModel: ObservableObject {
@Published var todo: [ToDo] = []
func groupByDay()->[Int:[ToDo]] {
let calendar = Calendar.current
let g: [Int:[ToDo]] = todo.reduce([:]) { (res, todo) in
var res = res
let i = calendar.ordinality(of: .day, in: .era, for: todo.date) ?? 0
var arr = res[i] ?? []
arr.append(todo)
arr.sort { (a, b) -> Bool in
a.date < b.date
}
res.updateValue(arr, forKey: i)
return res
}
return g
}
}
There is nothing special there, I will fill it with some randomly scheduled tasks later and I defined in model a function which return dictionary of sorted tasks array, where dictionary key is based on date portion of scheduled Date (date and time). All tasks will be randomly scheduled in interval 0 to 50000 seconds from "now"
Rest is SwiftUI code, which is "self explanatory"
struct ContentView: View {
@ObservedObject var model = ToDoModel()
var body: some View {
VStack {
Button(action: {
let todos: [ToDo] = (0 ..< 5).map { (_) in
ToDo(date: Date(timeIntervalSinceNow: Double.random(in: 0 ... 500000)), task: "task \(Int.random(in: 0 ..< 100))")
}
self.model.todo.append(contentsOf: todos)
}) {
Text("Add 5 random task")
}.padding()
Button(action: {
self.model.todo.removeAll { (t) -> Bool in
t.done == true
}
}) {
Text("Remove done")
}.padding()
List {
ForEach(model.groupByDay().keys.sorted(), id: \.self) { (idx) in
Section(header: Text(self.sectionDate(section: idx)), content: {
ForEach(self.model.groupByDay()[idx]!) { todo in
ToDoView(todo: todo).environmentObject(self.model)
}
})
}
}
}
}
// this convert back section index (number of days in current era) to date string
func sectionDate(section: Int)->String {
let calendar = Calendar.current
let j = calendar.ordinality(of: .day, in: .era, for: Date(timeIntervalSinceReferenceDate: 0)) ?? 0
let d = Date(timeIntervalSinceReferenceDate: 0)
let dd = calendar.date(byAdding: .day, value: section - j, to: d) ?? Date(timeIntervalSinceReferenceDate: 0)
let formater = DateFormatter.self
return formater.localizedString(from: dd, dateStyle: .short, timeStyle: .none)
}
}
struct ToDoView: View {
@EnvironmentObject var model: ToDoModel
let todo: ToDo
var body: some View {
VStack {
Text(todoTime(todo: todo)).bold()
Text(todo.task).font(.caption)
Text(todo.done ? "done" : "active").foregroundColor(todo.done ? Color.green: Color.orange).onTapGesture {
let idx = self.model.todo.firstIndex { (t) -> Bool in
t.id == self.todo.id
}
if let idx = idx {
self.model.todo[idx].done.toggle()
}
}
}
}
// returns time string
func todoTime(todo: ToDo)->String {
let formater = DateFormatter.self
return formater.localizedString(from: todo.date, dateStyle: .none, timeStyle: .short)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you like to use toggle, you have to be careful, otherwise removing tasks assigned as "done" will crash.
struct ToDoView: View {
@EnvironmentObject var model: ToDoModel
let todo: ToDo
var idx: Int? {
self.model.todo.firstIndex { (t) -> Bool in
t.id == self.todo.id
}
}
var body: some View {
VStack(alignment: .leading) {
Text(todoTime(todo: todo)).bold()
Text(todo.task).font(.caption)
Text(todo.done ? "done" : "active").foregroundColor(todo.done ? Color.green: Color.orange).onTapGesture {
self.model.todo[self.idx!].done.toggle()
}
// using toggle needs special care!!
// we have to "remove" it before animation transition
if idx != nil {
Toggle(isOn: $model.todo[self.idx!].done) {
Text("done")
}
}
}
}
// returns time string
func todoTime(todo: ToDo)->String {
let formater = DateFormatter.self
return formater.localizedString(from: todo.date, dateStyle: .none, timeStyle: .short)
}
}
On 11.3 there is other "trick" required, see SwiftUI Toggle in a VStack is misaligned for further details.
Upvotes: 2