I'm new to swift and I have trouble with understanding how environment variables works.
In Core Data, I created new Entity called "API" with one attribute id: Int32.
Then in SwiftUI, I wanted to find maximum value of id. I wrote a request, but whenever I used passed to view as environment variable managedObjectContext, it always crashed my app/preview. Here's crash info after using NSManagedObjectContext.fetch(NSFetchRequest) (using FetchRequest gives only stacktrace with exception EXC_BAD_INSTRUCTION)
...
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
External Modification Warnings:
Thread creation by external task.
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The fetch request's entity 0x600003c54160 'API' appears to be from a different NSManagedObjectModel than this context's'
terminating with uncaught exception of type NSException
abort() called
CoreSimulator 704.12 - Device: iPhone 11 (8356FF2A-5F0A-42F7-AA32-396FADCF2BF6) - Runtime: iOS 13.4 (17E255) - DeviceType: iPhone 11
Application Specific Backtrace 1:
0 CoreFoundation 0x00007fff23e3dcce __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff50b3b9b2 objc_exception_throw + 48
2 CoreData 0x00007fff239c6b99 -[NSManagedObjectContext executeFetchRequest:error:] + 5004
3 libswiftCoreData.dylib 0x00007fff513b63d4 $sSo22NSManagedObjectContextC8CoreDataE5fetchySayxGSo14NSFetchRequestCyxGKSo0gH6ResultRzlF + 68
...
Keep in mind, that this error is changing depending on which project, I'm using. In my main project I had error like that:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'WebsiteAPI''
Here is the code I'm using
import SwiftUI
import CoreData
struct test: View {
private var id: Int32
#Environment(\.managedObjectContext) var managedObjectContext
var body: some View {
Text("id=\(id)")
}
public init(context: NSManagedObjectContext) {
self.id = -1
//this crashes and gives no usefull information
// let request2 = FetchRequest<API>(
// entity: API.entity(),
// sortDescriptors: [NSSortDescriptor(keyPath: \API.id, ascending: false)]
// )
// self.id = request2.wrappedValue.first?.id ?? 1
guard let context2 = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
fatalError("Unable to read managed object context.")
}
let request = NSFetchRequest<API>(entityName: "API")
request.sortDescriptors = [NSSortDescriptor(keyPath: \API.id, ascending: false)]
do {
var commits = try context.fetch(request) // OK
commits = try context2.fetch(request) // OK
//commits = try self.managedObjectContext.fetch(request) // causing crash
self.id = Int32(commits.count)
} catch let error {
print(error.localizedDescription)
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
fatalError("Unable to read managed object context.")
}
return test(context: context).environment(\.managedObjectContext, context)
}
}
All commented lines crash app. Why getting context from AppDelegate.persistentContainer.viewContext works just fine, but using environment variable managedObjectContext, which in my opinion should be same, doesn't work? I spent 5 hours on this, checked pretty much everything, tried a lot of things but with no success. In the end I can just keep getting context from AppDelegate, but what's wrong with environment variable? Am I missing some common knowledge or is just a bug? I'm getting headache from bugs that I'm encountering in Xcode, starting from missing autocompletion after clearing build folder to hundreds of errors after changing struct/file name on all references, despite successfully building afterwards. Restarting Xcode few times every day to make it working properly is normal for me.
Also some things I noticed, when I created FetchRequest as a variable and used it in some list inside body, it worked. The problem is only, when I'm trying to fetch things manually in code/function/init, like button action or methods onAppear, init etc. I tried to run app on both physical device and showing preview. Same effect.
I'm using Xcode 11.4 with Swift 5.
Structs like the View in SwiftUI are value types and must not init any objects in the normal way because the View structs are only created during a state change and then are gone. Thus any objects they create are lost immediately. E.g. in your init method you create NSFetchRequest, NSSortDescriptor and all the fetched objects too. A View struct is typically init every time there is a state change and a parent body runs, thus you will be creating thousands of heap objects that will fill up memory and slow SwiftUI to a crawl. These problems can be diagnosed in Instruments->SwiftUI "analysis for tracing .body invocations for View types".
Obviously we do need to objects so that's where property wrappers come in. By prefixing your object allocation with a property wrapper, then the object is created in a special way where it is only init once and the same instance is given to the new struct every time it is recreated. Which as I said happens all the time in SwiftUI, more frequently or less frequently depending on how much effort you put into organising your View struct hierarchy. Health warning: Most of the sample code currently available online puts zero effort into this and needlessly update massive view hierarchies because the designed their View structs like View Controllers instead of making them as small as possible and only having properties that are actually used in body.
To solve your problem you need to use the property wrapper #StateObject to safely init your object, and it must conform to ObservableObject so that SwiftUI can be notified that the object will be changing so that after all objects have notified it can call body which will certainly be needed, unless the developer did not use the object in their body in which case the code is badly written. The object is created once just before the View's body is called, and then every time the Viewis recreated it is given the existing object rather than creating a new one. When the view is no longer shown it is automatically deinit. UseonAppearto configure the object the first time the View appears, andonChangeto update it. Fust have a funcfetchthat supplies themanagedObjectContextand youridfetch param and in it create aNSFetchedResultsControllerperform the fetch and set thefetchedObjectson an#Publishedproperty that theViewcan use. When the object sets its items it will automatically cause theView bodyto be called again updating. SwiftUI compares the body to the previously returned body and uses the differences to render the screen (using actualUIView`s). Here is a full working example I made:
import SwiftUI
import CoreData
struct ContentView: View {
var body: some View {
NavigationView {
MasterView(name:"Master")
.navigationTitle("Master")
}
}
}
class ItemsFetcher : NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
var managedObjectContext : NSManagedObjectContext?
#Published
private(set) var items : Array<Item> = []
lazy var fetchedResultsController : NSFetchedResultsController<Item> = {
let frc = NSFetchedResultsController<Item>(fetchRequest: Item.myFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
func fetch(name:String, ascending: Bool){
fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "name = %#", name)
fetchedResultsController.fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: ascending)]
try! fetchedResultsController.performFetch()
items = fetchedResultsController.fetchedObjects ?? []
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
objectWillChange.send()
items = fetchedResultsController.fetchedObjects ?? []
}
}
struct MasterView: View {
#Environment(\.managedObjectContext) private var viewContext
let name: String
#State
var ascending = false
#StateObject private var itemsFetcher = ItemsFetcher()
var body: some View {
List {
ForEach(itemsFetcher.items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigation){
EditButton()
}
#endif
ToolbarItem(placement: .automatic){
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem(placement: .bottomBar){
Button(action: {
ascending.toggle()
}) {
Text(ascending ? "Descending" : "Ascending")
}
}
}
.onAppear() {
itemsFetcher.managedObjectContext = viewContext
fetch()
}
.onChange(of: ascending) { newValue in
fetch()
}
}
func fetch(){
itemsFetcher.fetch(name: name, ascending: ascending)
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.name = "Master"
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map {itemsFetcher.items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Related
Since I use other extensions in my app, I want to define my CoreData and all the associated logic in a separate framework, which is then accessed by the main app, as well as all extensions.
When I want to access this from my main app I get the error message:
No NSEntityDescriptions in any model claim the NSManagedObject
subclass 'FrameworkName.EntityName' so +entity is confused. Have you
loaded your NSManagedObjectModel yet ?
As the error message says, probably the Main app does not know the CoreData model, because it is not loaded there.
In other SO posts [1, 2] it is written that you have to make changes in AppDelegate. I added the following code hoping that this will load the CoreData, unfortunately it does not. Can anyone help.
#main struct WebListsApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
FolderView()
}
} }
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModelName")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
return container
}()
lazy var managedObjectModel: NSManagedObjectModel = {
let frameworkBundleIdentifier = "frameworkIdentifier"
let customKitBundle = Bundle(identifier: frameworkBundleIdentifier)!
let modelURL = customKitBundle.url(forResource: "DataModelName", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
}
I am trying to get more than one entity for my coding project at school but I have an error saying invalid redeclaration of data controller.
class DataController: ObservableObject{
let container = NSPersistentContainer(name: "Blood Sugar")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
class DataController : ObservableObject{
let containers = NSPersistentContainer(name: "Carbohydrates")
init(){
containers.loadPersistentStores{ description, errors in
if let errors = errors{
print("Core data failed to load: \(errors.localizedDescription)")
}
}
}
}
Since you tagged this as SwiftUI, DataController should be a struct. We use value types like structs now to solve a lot of the bugs caused by using objects in UIKit and ObjC. You can see Apple's doc Choosing Between Structures and Classes for more info.
If you use an Xcode app template project and check "Use core data" you'll see a PersistenceController struct that will demonstrate how to do it correctly. I've included it below:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "SearchTest")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
You can create more entities in the model editor. There is usually only one NSPersistentContainer per app. The container can have multiple stores (usually sqlite databases). Then you can assign different entities to each store too. To create an instance of an entity you do that on a NSManagedObjectContext and you can choose which store to save it too, although most of the time people use one store which is the default.
I have very simple app, that has a CoreData database and in it, it has two type of objects, one that is the main object, and the other. The main object, Object A, can have many Object B. But Object B, can be connected to only one Object A.
My problem is, after a while of the app running, it runs into EXC_BAD_ACCESS error.
To be precise:
Thread 85: EXC_BAD_ACCESS (code=1, address=0x77e341213c20)
I have done some debugging, and it looks like this only happens, when I open the SwiftUI part of the interface, and possibly make changes to the db. I have read in forums, that it's a Thread issue and access. I tried database setup mentioed there (I am copying here) but that still runs into the error.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
return result
}()
var context: NSManagedObjectContext {
return container.viewContext
}
var container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp")
// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.newBackgroundContext()
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: {(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
My question is, how can I debug this? I understand the fact, this is trying to access the array of Objects B, that is already released, I just don't understand why it's released. Could it be because I opened the SwiftUi window and closed it? But why doesn't the connection just keep it?
Is there a way to prevent this error? I can see 3 threds running in the debugger, when the excecption is thrown, but I'm not aware of "creating new thread" and being new to Swift, not sure how to start a one, or stop one from being created.
Apart from passing the context down directly to the view, in two places I use a helper that looks like this:
public func getManagedContext() -> NSManagedObjectContext {
return (NSApplication.shared.delegate as! AppDelegate).coreDataStack.context
}
You can try fetching values in DipatchQueue.main block
it will avoid blocking the current thread.
I'm attempting to set up CoreData/CloudKit on a WatchOS application using NSPersistentCloudKitContainer. However, after calling container.loadPersistentStores, the viewContext.persistentStoreCoordinator field appears to still be nil.
Here are some code samples:
Persistence.swift
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init() {
container = NSPersistentCloudKitContainer(name: "Test")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("Could not retrieve a persistent store description.")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
MainApp.swift
#main
struct EventLoggerApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
ContentView.swift
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
func createRecord() {
print(viewContext.persistentStoreCoordinator) // this is nil
let sensorReadingDescription = NSEntityDescription.entity(forEntityName: "SensorReading", in: viewContext)! // this crashes
}
}
The error I'm crashing with is:
2020-12-12 11:56:32.205433-0800 EventLoggerWatch WatchKit Extension[400:541990] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Test''
*** First throw call stack:
(0x1f4bb7dc 0x1edc9c04 0x23d68a44 0x174d34 0x174378 0x22fa9d6c 0x231422e0 0x23256ebc 0x22cb86a8 0x23256ed8 0x23256ebc 0x2327a6a4 0x2330f2dc 0x2324cdac 0x2324b6f4 0x2324b75c 0x2324bd20 0x4076df2c 0x40bd9f44 0x40763d70 0x40763e80 0x40763ce8 0x40b986cc 0x40b75abc 0x40becf04 0x40be59b4 0x1f43c4ec 0x1f43c3ec 0x1f43b74c 0x1f435dac 0x1f435574 0x235f94c0 0x40b580f8 0x40b5d5f8 0x350d9b68 0x1eed893c)
libc++abi.dylib: terminating with uncaught exception of type NSException
Please let me know if I can provide any more helpful context, and thanks in advance for the guidance!
Upon further investigation, I'm not sure this question is valid. I think the code listed above was only running for the phone target, and not the watch. There are potentially issues with the watch as well, but I will work on getting the phone working first, as the issues are probably there. Thanks to all who helped out here!
I wrote a function to fetch database in CoreData. this function will take a closure and run performBackgroundTask to fetch the data. Then, passing the result to the closure to run.
I wrote static properties in AppDelegate for me to access viewContext easily:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static var persistentContainer: NSPersistentContainer {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer
}
static var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
// ...
}
The following is the function(not method) I wrote which crashed by using context:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? context.fetch(fetchRequest) else { // crash
return
}
context.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
but if i replace context with AppDelegate.viewContext, the function won't crash:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? AppDelegate.viewContext.fetch(fetchRequest) else { // crash
return
}
AppDelegate.viewContext.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
what is exactly going on?
thanks.
Here are some issues:
performBackgroundTask is already on the right thread for the context so there is no reason to call context.performAndWait and may lead to a deadlock or a crash.
The items fetched or created in a performBackgroundTask cannot leave that block under any circumstances. The context will be destroyed at the end of the block and the managedObjects will crash when it tries to access its context
Managing core-data thread safety can be difficult and I have found it a generally good practice to never pass or return managed objects to functions, unless the context of the object is explicit and clear. This is not an unbreakable rule, but I think it is a good rule of thumb when making your APIs.
performBackgroundTask is generally used for updates to core data. If you are only doing fetches you should use the viewContext. Doing a fetch on the background only to pass it to the main thread is generally a waste.
While in a performBackgroundTask block you cannot access the viewContext - neither for reading or for writing. If you do the app can crash any at time with confusing crash reports, even at a later time when you are not violating thread safety.
I don't know what the predicates that you are creating look like, but I have a strong feeling that they are wrong. This would cause a crash when fetching.
Overall I think that the function you created has little value. If all it is doing is a fetch then you should simply create the predicate and sort descriptors and fetch on the viewContext. If you insist on keeping the function, then remove the performBackgroundTask, fetch using the viewContext, return the results(instead of a callback) and only call it from the main thread.