每次尝试将项目添加到分组项目时,SwiftUI 应用程序都会崩溃

Posted

技术标签:

【中文标题】每次尝试将项目添加到分组项目时,SwiftUI 应用程序都会崩溃【英文标题】:SwiftUI app crashes every time when trying to add item to grouped items 【发布时间】:2020-03-24 10:15:40 【问题描述】:

当我尝试在 ForEach 循环中按日期对项目进行排序时,我的应用程序崩溃并出现以下错误:

2020-03-24 16:55:13.830146+0700 列表日期[60035:2135088] *** -[_UITableViewUpdateSupport 中的断言失败 _setupAnimationsForNewlyInsertedCells],/BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3901.4.2/UITableViewSupport.m:1311 (lldb)

上线:

class AppDelegate: UIResponder, UIApplicationDelegate 

AppDelegate.swift

起初我的应用程序在模拟器中加载,但在我点击添加按钮打开模式窗口并添加新项目后,应用程序立即崩溃并出现错误。

我认为 func 更新或 ForEach 循环本身存在问题。我在代码中标记了哪个替代循环对我有用。可悲的是,这种替代方法不会按日期对项目进行分组。这是我尝试在我的应用中添加的功能。

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

我正在使用 CoreData。在此示例中,有 1 个名为 Todo 的实体,具有 2 个属性:日期(Date)、标题(字符串)。

如果有人能帮助我解决这个错误,我将不胜感激。或者分组项目的替代方案也可以工作:)

【问题讨论】:

首先,ForEach 不是任何类型的循环...您必须改变主意并尝试理解 SwftUI 的真正声明性语法的含义以及如何使用它。第二步是将您的业务逻辑与用户界面隔离,将所有功能转移到您的模型中……接下来的生活将变得更加轻松,您的代码将合乎逻辑且易于维护。没有简单的方法可以将当前的 UIKit 代码更改为 SwiftUI。 感谢您的反馈。我不明白这个“第二步”部分。我从一开始就在 SwiftUI 中编写这个应用程序。我是斯威夫特的新手。我不知道如何使用 ForEach 对项目进行分组。我在这里找到了解决方案:***.com/questions/59180698/… 但在将它与我的代码一起使用后,它会使我的应用程序崩溃并出现错误。我认为这是因为使用了 .sheet 。它看起来不是一个 SwiftUI 解决方案,但我还找不到更好的解决方案。如果你知道,请分享你的知识。谢谢:) 按某些参数分组数据不是与 UI 相关的任务,因此请在您的模型中进行。完成后,在 UI 中显示结果。最糟糕的想法是调用 ForEach 构造函数作为另一个 ForEach 构造函数的一部分。完全避免这种情况,即使在某些情况下可能会起作用。避免(如果可用) id:\self 作为 ForEach 构造函数的一部分。很难修复你的代码,只是因为我们对你的数据一无所知...... 谢谢。如何在模型中做到这一点?您的意思是将 Codegen 从 Class Definition(我目前使用的)更改为 Manual/None 并在那里添加一些代码?你能指导我一些关于你的解决方案的教程吗?如果将来我想通过 UI 动态更改分组怎么办?现在我想按日期对项目进行分组。但是,例如,如果我想允许用户将分组从日期更改为类别,该怎么办。通过数据模型处理分组是否比 UI 更好? 避免 id:\self 作为 ForEach 构造函数的一部分的原因是什么?我在很多例子中看到它?我想了解更多。 【参考方案1】:

为了获得灵感,如何使用你的模型,这里是简化的例子

import SwiftUI // it imports all the necessary stuff ...

我们的任务需要一些符合 Identifiable 的数据结构(这将有助于 SwiftUI 识别每个动态生成的 ToDoView)

struct ToDo: Identifiable 
    let id = UUID()
    let date: Date
    let task: String
    var done = false

具有所有基本功能的简单模型可以定义为

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
    

那里没有什么特别的,我稍后会用一些随机安排的任务填充它,我在模型中定义了一个返回排序任务数组字典的函数,其中字典键基于预定日期的日期部分(日期和时间) .所有任务将从“现在”开始以 0 到 50000 秒的间隔随机调度

Rest 是 SwiftUI 代码,“不言自明”

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

如果你喜欢使用切换,你必须小心,否则删除分配为“完成”的任务会崩溃。

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)
    

在 11.3 上还需要其他“技巧”,请参阅 SwiftUI Toggle in a VStack is misaligned 了解更多详情。

【讨论】:

首先,非常感谢您指出这个“切换”问题。我在 4 个月前发现了这个错误:***.com/questions/58965282/… 不知道这个解决方案。我已立即将此错误提交给 Apple。几天前他们联系我说无法重现该问题。我已经向他们发送了仍然存在问题的简化代码和问题的屏幕截图,希望他们能尽快修复它,但是......现在已经 4 个月了,它仍然无法正常工作。在 ios 13.1 模拟器上运行良好,你知道吗? @mallow 它似乎适用于他们昨天发布的 Xcode 版本 11.4 (11E146) 至于您对我的问题的回答...感谢您的所有时间。您的代码有效。而且看起来也不错。但由于我是 Swift 新手,我需要更多时间来测试它。我的原始代码不适用于 .sheet 模态。它使用了 CoreData。我仍然不知道如何翻译您的代码以与核心数据一起使用。完成后,我将处理模态。所以...我尝试注释掉 struct ToDo 并改为创建一个 CD 实体。但仍然不知道下一步该做什么。需要更多时间,抱歉:/ 我仍在尝试寻找如何将您的答案与 CoreData 一起使用的解决方案。如有任何建议,我将不胜感激。

以上是关于每次尝试将项目添加到分组项目时,SwiftUI 应用程序都会崩溃的主要内容,如果未能解决你的问题,请参考以下文章

Swift:将 SwiftUI 元素添加到 UIKit 项目:无法正确应用约束

SwiftUI Preview Canvas 构建失败,但项目构建没有

SwiftUI:将列表添加到带有上方图标的滚动视图中

SwiftUI:将项目动态添加到 TabView [重复]

将 AdMob 添加到 Firebase 项目时崩溃

我在将图像添加到颤振项目时遇到问题