1

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 :)

mallow
  • 2,368
  • 2
  • 22
  • 63
  • 1
    At first, ForEach is NOT any kind of loop ... You have to change your mind and try to understand what really declarative syntax of SwftUI means and how to use it. The second step is to isolate your business logic from user interface, move all functionality to your model ... The life next will be much easier, your code will be logical and easy to maintain. There is no easy way to change current UIKit code to SwiftUI. – user3441734 Mar 24 '20 at 11:20
  • Thanks for your feedback. I don't understand this "second step" part. I am writing this app in SwiftUI from the beginning. I am new to Swift. I don't know how to group items using ForEach. I have found a solution here: https://stackoverflow.com/questions/59180698/how-to-properly-group-a-list-fetched-from-coredata-by-date but after using it with my code it makes my app crash with an error. I think this is because the .sheet used. It looks as not a SwiftUI solution, but I cannot find a better one yet. If you know, please share your knowledge. Thanks :) – mallow Mar 24 '20 at 11:28
  • 1
    group data by some parameters is not task related to UI, so do it in your model. once it is done, present the result in UI. The worst idea is call ForEach constructor as part of another ForEach constructor. Avoid that at all, even though is could work in some cases. Avoid (if available) id:\self as part of ForEach constructor. There is hard to fix you code, just because we don't have any idea about your data ... – user3441734 Mar 24 '20 at 11:36
  • Thanks. How to do it in model? Do you mean changing Codegen from Class Definition (as I currently use) to Manual/None and adding some code there? Could you direct me to some tutorials about your solution? And what if in the future I would like to dynamically change grouping with UI? Right now I would like to group items by date. But what if I would like to allow the users changing grouping from date to category for example. Is it still better to handle grouping through data model than UI? – mallow Mar 24 '20 at 11:58
  • And what's the reason to avoid id:\self as part of ForEach constructor? I saw it in many examples? I would like to learn more about it. – mallow Mar 24 '20 at 12:16
  • there is no reason if you are sure that self really identify the data element (which you have to guarantee). if you are able make your data conforming to Identifiable, just do it for your advantage. – user3441734 Mar 24 '20 at 13:04
  • and for previous notes, yes, do it in your model, test it separately and if everything works, design UI as simple as possible, taking the advantage of SwiftUI declarative syntax – user3441734 Mar 24 '20 at 13:07
  • i made simple example, see my answer ... to be inspired – user3441734 Mar 24 '20 at 16:26
  • Thank you! :) I will check it early morning – mallow Mar 24 '20 at 16:29
  • I still have a problem with advice to avoid id:\.self. In this video Paul Hudson explains why it is safe to use .self with ForEach and CoreData: https://youtu.be/TtaW6VqyUxE. Why it’s important is that your answer uses structs. I need to use CoreData which uses classes. Classes are not identifiable. They are however hashable. It seems that I can use .self, but please correct me if I am mistaken – mallow Mar 25 '20 at 07:10
  • in the video he sad exactly what i mentioned in our discussion above "there is no reason if ... " – user3441734 Mar 25 '20 at 07:25
  • and check that even in my example i used .\self too :-) related to keys in "sections" dictionary. that's because I am sure all my dictionary keys are unique (by definition) – user3441734 Mar 25 '20 at 07:28
  • You’re right :) Thanks for further explorations. I just try to understand the best practices. And to make use of your code in my app, using core data. So .self may cause problems if some keys are not unique. But in your code they are. And CoreData entries are too, as said in the video. – mallow Mar 25 '20 at 07:52

1 Answers1

2

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()
    }
}

enter image description here

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.

user3441734
  • 16,722
  • 2
  • 40
  • 59
  • At first, thank you very much for pointing to this "Toggle" question. I found this bug 4 months ago: https://stackoverflow.com/questions/58965282/problems-with-layout-of-some-rows-in-swiftui-list Didn't know about this solution. I have submitted this bug to Apple immediately. They contacted me few days ago saying that cannot reproduce the problem. I have sent them simplified code that still has the issue and screenshots with the problem and hope they will fix it soon, but... it is 4 months now and it still doesn't work. On iOS 13.1 simulator it works good, you know? – mallow Mar 25 '20 at 05:27
  • 1
    @mallow it seems to work on Xcode Version 11.4 (11E146) which they released yesterday – user3441734 Mar 25 '20 at 05:31
  • As for your answer to my problem... Thanks for all your time.Your code works. And looks good too. But as I am novice with Swift, I need more time to test it. My original code didn't work with .sheet modal. And it used CoreData. I still don't know how to translate your code to use with core data. After I will do it, I will handle the modal. So... I try to comment out the struct ToDo and make a CD entity instead. But still don't know what to do next. Need more time, sorry :/ – mallow Mar 25 '20 at 05:31
  • I am still trying to find a solution on how to use your answer with CoreData. I would appreciate any suggestions. – mallow Mar 27 '20 at 02:40