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?
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.