Error Domain=NSCocoaErrorDomain Code=134030“保存时发生错误。”当尝试通过 UITableViewDiffableDataSource 删除 Core Data 对象时

问题描述 投票:0回答:1

我正在尝试做的事情:

我正在尝试学习如何将 Core Data 与 NSFetchedResultsController 和 UITableViewDiffableDataSource 结合起来,以创建一个简单的表 UI,以顺利处理 Core Data 中项目表的添加和删除。

我首先构建了一些核心数据代码,然后添加了一个 TableView,首先将其与手动获取挂钩,然后移动到 NSFetchedResultsController,使用“旧”批量更新行为,现在我正在移动该代码使用 DiffableDataSource。

错误:

我有两个用于删除核心数据存储中的项目的函数。可以通过包含删除全部按钮的秘密菜单(通过摇动访问),也可以通过经典的滑动删除手势。

使用“全部删除”按钮似乎永远不会出错,但滑动删除按钮大约 50% 的情况下会出错。

错误信息如下:

Fatal error: Unable to delete note: Error Domain=NSCocoaErrorDomain Code=134030 "An error occurred while saving." UserInfo={NSAffectedObjectsErrorKey=(
    "<Note: 0x6000013af930> (entity: Note; id: 0x6000030f9ba0 <x-coredata:///Note/t82D95BE5-DDAE-4684-B19E-4CDA842DF89A2>; data: {\n    creation = nil;\n    text = nil;\n})"
)}: file /Users/philip/Git/Noter/Noter/Noter/Note+CoreDataClass.swift, line 58

编辑:我已经进一步缩小了错误条件的范围。

该错误仅在我尝试删除最近创建的对象时才会发生。

也就是说,创建一个新笔记,然后删除同一个笔记。

如果我创建两个注释,并删除第一个创建的注释,则不会出现错误。删除之后,我可以类似地进去删除第二个笔记,没有任何错误。

同样,如果我创建一条笔记,停止模拟器,然后重新启动它,从核心数据加载该笔记,我就可以毫无问题地删除该笔记。

我尝试过的:

基于错误仅在使用滑动删除时发生的事实,我认为问题可能以某种方式与 UITableViewDiffableDataSource 相关,而 UITableViewDiffableDataSource 仍然具有低于标准的文档。

我尝试使用 SQL 查看器 (Liya) 检查我的核心数据数据库,该数据库似乎具有我期望的记录,并且我看到它们被正确创建和删除,同时使用我的调试菜单创建 1 或 1000 条记录,使用调试菜单删除所有记录看起来也很正确。

我启用了

-com.apple.CoreData.SQLDebug 1
参数来查看 SQL 输出。再次看来插入和删除工作正常。除非发生错误。

我也尝试使用调试器并逐步解决问题,尽管由于某些奇怪的原因,似乎在断点处使用

frame variable
lldb 命令(
tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool
函数的开头)导致断点下降通过,代码继续处理错误。

代码:

这是我认为相关代码的摘录,完整源代码也可在 https://github.com/Hanse00/Noter/tree/master/Noter/Noter 获得。如果您能帮助我理解这个问题,我将不胜感激。

ViewController.swift


import UIKit
import CoreData

// MARK: - UITableViewDiffableDataSource
class NoteDataSource<SectionIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, NSManagedObjectID> where SectionIdentifierType: Hashable {
    var container: NSPersistentContainer!

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            guard let objectID = itemIdentifier(for: indexPath) else {
                fatalError("Unable to find note for indexPath: \(indexPath)")
            }

            guard let note = container.viewContext.object(with: objectID) as? Note else {
                fatalError("Could not load note for id: \(objectID)")
            }

            Note.delete(note: note, from: container)
        }
    }
}

// MARK: - ViewController
class ViewController: UIViewController {
    lazy var formatter: DateFormatter = {
       let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        return formatter
    }()
    lazy var fetchedResultsController: NSFetchedResultsController<Note> = {
        let controller = NSFetchedResultsController(fetchRequest: Note.sortedFetchRequest(), managedObjectContext: self.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        controller.delegate = self
        return controller
    }()
    var container: NSPersistentContainer!
    var dataSource: NoteDataSource<Section>!
    @IBOutlet var tableView: UITableView!

    enum Section: CaseIterable {
        case main
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configureDataSource()
        tableView.delegate = self
    }

    override func viewDidAppear(_ animated: Bool) {
        do {
            try fetchedResultsController.performFetch()
        } catch {
            fatalError("Failed to fetch entities: \(error)")
        }
    }

    func configureDataSource() {
        dataSource = NoteDataSource(tableView: tableView, cellProvider: { (tableView: UITableView, indexPath: IndexPath, objectID: NSManagedObjectID) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: "NoteCell", for: indexPath)

            guard let note = self.container.viewContext.object(with: objectID) as? Note else {
                fatalError("Could not load note for id: \(objectID)")
            }

            cell.textLabel?.text = note.text
            cell.detailTextLabel?.text = self.formatter.string(from: note.creation)

            return cell
        })
        dataSource.container = container
    }

    // MARK: User Interaction

    override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
        if motion == .motionShake {
            addRandomNotePrompt()
        }
    }

    func addRandomNotePrompt() {
        let alert = UIAlertController(title: "Add Random Note", message: nil, preferredStyle: .actionSheet)
        let addAction = UIAlertAction(title: "Add a Note", style: .default) { (action) in
            Note.createRandomNote(in: self.container)
        }
        let add1000Action = UIAlertAction(title: "Add 1000 Notes", style: .default) { (action) in
            Note.createRandomNotes(notes: 1000, in: self.container)
        }
        let deleteAction = UIAlertAction(title: "Delete all Notes", style: .destructive) { (action) in
            Note.deleteAll(in: self.container)
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

        alert.addAction(addAction)
        alert.addAction(add1000Action)
        alert.addAction(deleteAction)
        alert.addAction(cancelAction)
        present(alert, animated: true)
    }
}

// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }

}

// MARK: - NSFetchedResultsControllerDelegate
extension ViewController: NSFetchedResultsControllerDelegate {

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
        dataSource.apply(snapshot as NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>, animatingDifferences: true)
    }

}

Note+CoreDataClass.swift

import Foundation
import CoreData

@objc(Note)
public class Note: NSManagedObject {

    public class func fetchRequest() -> NSFetchRequest<Note> {
        return NSFetchRequest<Note>(entityName: "Note")
    }

    public class func sortedFetchRequest() -> NSFetchRequest<Note> {
        let request: NSFetchRequest<Note> = fetchRequest()
        let sort = NSSortDescriptor(key: "creation", ascending: false)
        request.sortDescriptors = [sort]
        return request
    }

    public class func createRandomNote(in container: NSPersistentContainer) {        
        let thingsILike = ["Trains", "Food", "to party party!", "Swift"]
        let text = "I like \(thingsILike.randomElement()!)"

        let dayOffset = Int.random(in: -365...365)
        let hourOffset = Int.random(in: -12...12)
        let dateOffsetDays = Calendar.current.date(byAdding: .day, value: dayOffset, to: Date())!
        let date = Calendar.current.date(byAdding: .hour, value: hourOffset, to: dateOffsetDays)!

        let note = Note(context: container.viewContext)
        note.creation = date
        note.text = text

        do {
            try container.viewContext.save()
        } catch {
            fatalError("Unable to save: \(error)")
        }
    }

    public class func createRandomNotes(notes count: Int, in container: NSPersistentContainer) {
        for _ in 1...count {
            createRandomNote(in: container)
        }
    }

    public class func delete(note: Note, from container: NSPersistentContainer) {
        do {
            container.viewContext.delete(note)
            try container.viewContext.save()
        } catch {
            fatalError("Unable to delete note: \(error)")
        }
    }

    public class func deleteAll(in container: NSPersistentContainer) {
        do {
            let notes = try container.viewContext.fetch(fetchRequest()) as! [Note]
            for note in notes {
                delete(note: note, from: container)
            }
        } catch {
            fatalError("Unable to delete notes: \(error)")
        }
    }

}

Note+CoreDataProperties.swift

import Foundation
import CoreData


extension Note {
    @NSManaged public var text: String
    @NSManaged public var creation: Date

}

谢谢!

ios swift core-data nsfetchedresultscontroller diffabledatasource
1个回答
0
投票

对于任何遇到与“我无法删除刚刚创建的对象”类似情况的人来说,我的问题是我使用了临时对象 ID。虽然我没有使用像 OP 这样的核心数据支持的表,但我怀疑他们的代码就是这种情况。任何对表执行完全重新加载的操作(例如删除其他行)都会从后备存储中使用永久 ID 重新加载表并解决问题。

为了解决我的问题,创建对象后,我现在使用新对象调用

NSManagedObjectContext.obtainPermanentIDs(for:)

guard let managedObject = MyManagedObject(context: managedObjectContext) else { throw anError }
// ... setup managedObject ...
try managedObjectContext.obtainPermanentIDs(for: [managedObject])
// managedObject.objectID now is a permanent id
try managedObjectContext.save()

对于OP的代码,添加对

obtainPermanentIDs(for:)
的调用将直接在
.save()
中的
createRandomNote()
之前:

do {
    try container.viewContext.obtainPermanentIDs(for: [note])
    try container.viewContext.save()
} catch {
    fatalError("Unable to save: \(error)")
}

(注意:我没有在OP的代码中对此进行测试,这只是我对问题所在的猜测。就我自己的情况而言,一旦我拥有永久ID,我就可以毫无问题地删除我刚刚创建的对象。)

© www.soinside.com 2019 - 2024. All rights reserved.