The iOS app I'm building iterates through a list of PhraseGroup objects defined in Core Data and displays the extraText contents associated with each PhraseGroup. This shows how PhraseGroup is defined:

extension PhraseGroup {

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

@NSManaged public var extraText: String
@NSManaged public var phraseGroupID: UUID
@NSManaged public var text: String
@NSManaged public var phrase: NSSet?
@NSManaged public var piece: Piece

I would like users to be able to long press any extraText entry in my list then edit this field in a modal sheet. I don't want to use a NavigationLink for this purpose, as I want to use such a link for other functions.

The following shows how I'm listing the PhraseGroups, and how I display the modal sheet:

import SwiftUI
import CoreData

struct PhraseGroupView: View {

@Environment(\.managedObjectContext) var moc
@Binding var phraseGroupViewAlertItem: AlertItem?
@State private var isEditMode: EditMode = .inactive
@State private var phraseGroupObjectID: NSManagedObjectID? = nil

private var fetchRequest: FetchRequest<PhraseGroup>
private var phraseGroups: FetchedResults<PhraseGroup> { fetchRequest.wrappedValue }

var body: some View {
NavigationView {
    VStack(spacing: 20){
        Section {
            List {
                ForEach (phraseGroups, id: (\PhraseGroup.phraseGroupID)) { phraseGroup in
                    HStack {
                        Text("\(phraseGroup.wrappedExtraText)")
                    }
                    .onLongPressGesture {
                        phraseGroupObjectID = phraseGroup.objectID
                    }
                }
                .onDelete(perform: delete)
            }
            .sheet(item: self.$phraseGroupObjectID) { objID in
                TestPhraseGroupEditView(phraseGroupObjectID: objID).environment(\.managedObjectContext, self.moc)
            }
        }
    }
    .navigationBarTitle("This is phraseGroup navBarTitle", displayMode: .inline)
    .navigationBarItems(leading:
                            HStack {
                                Button(action: {
                                    // yet to come
                                }) {
                                    Image(systemName: "plus").resizable()
                                        .frame(width: 16, height: 16)
                                        .aspectRatio(contentMode: .fit)
                                        .foregroundColor(.myKeyColor)
                                }
                            }, trailing:
                                HStack {
                                    EditButton()
                                        .frame(width: 60, height: 20)
                                        .aspectRatio(contentMode: .fit)
                                        .foregroundColor(.myKeyColor)
                                })
    .environment(\.editMode, self.$isEditMode)
  }
}

init (phraseGroupViewAlertItem: Binding<AlertItem?>, piece: Piece) {
    self._phraseGroupViewAlertItem = phraseGroupViewAlertItem
    fetchRequest = FetchRequest<PhraseGroup>(
        entity: PhraseGroup.entity(),
        sortDescriptors: [
            NSSortDescriptor(key: "extraText", ascending: true)
        ],
        predicate: NSPredicate(format: "piece == %@", piece)
        // 'piece' in the predicate above is the name of a Piece <- PhraseGroup relationship defined in Core Data
    )
}

Here is my modal sheet (very much bare bones at the moment, with no attempt at including a save function yet):

import SwiftUI
import CoreData

  struct TestPhraseGroupEditView: View {

@Environment(\.managedObjectContext) var moc
@State private var extraTextForEditing = ""
var phraseGroupObjectID: NSManagedObjectID!

var phraseGroup: PhraseGroup {
        moc.object(with: phraseGroupObjectID) as! PhraseGroup
    }

var body: some View {
    NavigationView {
        Form {
            Section {
                TextField("Extra Text", text: $extraTextForEditing)
            }
        }
    }
    .onAppear {
        phraseGroup.managedObjectContext!.performAndWait {
            extraTextForEditing = phraseGroup.extraText
        }
    }
  }
}

This sheet displays fine following a long press on the previous list. However, the moment I open the TextField for input, the app throws NSInternalInconsistencyException with the reason An NSManagedObjectContext's retain policy cannot be changed while it has registered objects. Trying using reset() first.

The setup for my container and managed object context are, I believe, quite conventional. The only slightly unusual feature is that I am adding a singleton PiecePlayer (an audio managing class for my app) as an environment object at the same time as I am adding the managed object context to the environment.

Here's the container part from AppDelegate.swift:

lazy var persistentContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: "AK6")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {

            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

Here's the setup of the context in SceneDelegate.swift:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?
var pp = PiecePlayer()
@Environment(\.managedObjectContext) var moc

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    
    let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(pp)

For what it's worth, I've tried setting context.retainsRegisteredObjects = false explicitly in SceneDelegate.swift, but this has no effect on suppressing this error. I believe this is the default setting in any case.

My use of performAndWait in onAppear() was prompted by warnings at https://davedelong.com/blog/2018/05/09/the-laws-of-core-data/ about the dangers of changing the 'same' object from different managed object contexts, but even after reading reports about modal views not inheriting Environment(\.managedObjectContext) because they're detached from the app's view hierarchy, I'm not convinced that is what has happened here. Just to be extra safe, I'm passing the managed object context to the modal view when I invoke it, and inspecting the modal view's managed object context in the debugger shows it is not nil, and it does not have a parent - both of which suggest to me that I'm dealing with the 'right' managed object context.

Is it possible that this exception would actually be benign if I could only catch it formally? If so, how and where could I catch the exception? Alternatively (and presumably preferably), is there a way I can avoid triggering this exception in the first place?

1

There are 1 answers

0
Chatanooga On

Newbie error. Failed to examine Original Exception Backtrace in Xcode after error occurred. When I did, I found I'd set moc.retainsRegisteredObjects = true in the app's initial window (not shown in my posting). Removing this fixed the problem.