每次尝试将项目添加到分组项目时,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 项目:无法正确应用约束