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 createNSFetchRequest
,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
body
@StateObject
to safely init your object, and it must conform toObservableObject 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. Use
onAppearto configure the object the first time the View appears, and
onChangeto update it. Fust have a func
fetchthat supplies the
managedObjectContextand your
idfetch param and in it create a
NSFetchedResultsControllerperform the fetch and set the
fetchedObjectson an
@Publishedproperty that the
Viewcan use. When the object sets its items it will automatically cause the
Viewto be called again updating. SwiftUI compares the body to the previously returned body and uses the differences to render the screen (using actual
UIView`s). Here is a full working example I made: