Why is the entityForName on Environment(\.managedObjectContext).wrappedValue
always nil?
I get this error +entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Project'
With @Environment(\.managedObjectContext) var viewContext
I don't get this error.
But I need to initialize the view with my controller that needs NSManagedObjectContext
to be passed.
Can someone help to understand why these two lines don't return the same object? Or is it the same?
@main
struct umbrellaApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) var viewContext // Works
@StateObject private var controller: ContentViewController
init() {
// Crashes
let viewContextValue = Environment(\.managedObjectContext).wrappedValue
let controller = ContentViewController(managedObjectContext: viewContextValue)
self._controller = StateObject(wrappedValue: controller)
}
var body: some View {
NavigationView {
Text("Hello World")
}
}
}
The initializer of ContentViewController
.
init(managedObjectContext: NSManagedObjectContext) {
self.managedObjectContext = managedObjectContext
self.projectsController = NSFetchedResultsController(fetchRequest: Project.projectsFetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil, cacheName: nil)
super.init()
projectsController.delegate = self
do {
try projectsController.performFetch()
self.projects = projectsController.fetchedObjects ?? []
} catch {
print("failed to fetch projects!")
}
}
Short answer, Environment
needs the @
, it is a wrapper. What you are trying to do isn't a documented use of Environment
https://developer.apple.com/documentation/swiftui/environment
Long answer,
You haven't provided a Minimal Reproducible product but here is what I see
let viewContextValue = Environment(\.managedObjectContext).wrappedValue
It doesn't work because as you know the @Environment
isn't available at this point or you would just use viewContext
.
let controller = ContentViewController(managedObjectContext: viewContextValue)
I see what you are trying to do here but as stated above @Environment
just isn't available during init
self._controller = StateObject(wrappedValue: controller)
And this while it "works" on the surface it kind of defeats the virtues of StateObject
SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view. Instead, SwiftUI provides the StateObject attribute for this purpose. You can safely create a Book instance inside a view this way:
@StateObject var book = Book()
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
In my experience custom init
s in SwiftUI don't provide a reliable experience. I try to stay away from them as much as I can. If you have to do custom work upon init
do it in a class
as a ViewModel
/ViewController
that is also an ObservableObject
a View
shouldn't do any work.
If you want an alternative to what you want to do this see this SO question
All you need is
let persistenceController = PersistenceController.shared
Inside your ContentViewController
and initialize your StateObject
like this.
@StateObject private var controller: ContentViewController = ContentViewController()
Here is a sample where I used a FetchedResultsController
it has sections
import SwiftUI
import CoreData
class TaskListViewModel: ObservableObject {
let persistenceController = PersistenceController.previewAware()
@Published var fetchedResultsController: NSFetchedResultsController<Task>?
init() {
setupController()
}
func setupController() {
do{
fetchedResultsController = try retrieveFetchedController(sortDescriptors: nil, predicate: nil, sectionNameKeyPath: #keyPath(Task.isComplete))
}catch{
print(error)
}
}
func deleteObject(object: Task) {
persistenceController.container.viewContext.delete(object)
save()
}
func save() {
do {
if persistenceController.container.viewContext.hasChanges{
try persistenceController.container.viewContext.save()
objectWillChange.send()
}else{
}
} catch {
print(error)
}
}
}
//MARK: FetchedResultsController setup
extension TaskListViewModel{
func retrieveFetchedController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) throws -> NSFetchedResultsController<Task> {
return try initFetchedResultsController(sortDescriptors: sortDescriptors, predicate: predicate, sectionNameKeyPath: sectionNameKeyPath)
}
private func initFetchedResultsController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) throws -> NSFetchedResultsController<Task> {
fetchedResultsController = getFetchedResultsController(sortDescriptors: sortDescriptors, predicate: predicate, sectionNameKeyPath: sectionNameKeyPath)
//fetchedResultsController!.delegate = self
do {
try fetchedResultsController!.performFetch()
return fetchedResultsController!
} catch {
print( error)
throw error
}
}
func getFetchedResultsController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) -> NSFetchedResultsController<Task> {
return NSFetchedResultsController(fetchRequest: getEntityFetchRequest(sortDescriptors: sortDescriptors, predicate: predicate), managedObjectContext: persistenceController.container.viewContext, sectionNameKeyPath: sectionNameKeyPath, cacheName: nil)
}
private func getEntityFetchRequest(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?) -> NSFetchRequest<Task>
{
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
fetchRequest.includesPendingChanges = false
fetchRequest.fetchBatchSize = 20
if sortDescriptors != nil{
fetchRequest.sortDescriptors = sortDescriptors
}else{
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Task.dateAdded), ascending: false)]
}
if predicate != nil{
fetchRequest.predicate = predicate
}
return fetchRequest
}
}
struct TaskListView: View {
@StateObject var vm: TaskListViewModel = TaskListViewModel()
@State var taskToEdit: Task?
var body: some View {
if vm.fetchedResultsController?.sections != nil{
List{
ForEach(0..<vm.fetchedResultsController!.sections!.count){idx in
let section = vm.fetchedResultsController!.sections![idx]
TaskListSectionView(objects: section.objects as? [Task] ?? [], taskToEdit: $taskToEdit, sectionName: section.name).environmentObject(vm)
}
}.sheet(item: $taskToEdit, onDismiss: {
vm.save()
}){editingTask in
TaskEditView(task: editingTask)
}
}else{
Image(systemName: "empty")
}
}
}
struct TaskEditView: View {
@ObservedObject var task: Task
var body: some View {
TextField("name", text: $task.name.bound)
}
}
struct TaskListSectionView: View {
@EnvironmentObject var vm: TaskListViewModel
let objects: [Task]
@State var deleteAlert: Alert = Alert(title: Text("test"))
@State var presentAlert: Bool = false
@Binding var taskToEdit: Task?
@State var isExpanded: Bool = true
var sectionName: String
var body: some View {
Section(header: Text(sectionName) , content: {
ForEach(objects, id: \.self){obj in
let task = obj as Task
Button(action: {
taskToEdit = task
}, label: {
Text(task.name ?? "no name")
})
.listRowBackground(Color(UIColor.systemBackground))
}.onDelete(perform: deleteItems)
})
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
deleteAlert = Alert(title: Text("Sure you want to delete?"), primaryButton: Alert.Button.destructive(Text("yes"), action: {
let objs = offsets.map { objects[$0] }
for obj in objs{
vm.deleteObject(object: obj)
}
//Because the objects in the sections aren't being directly observed
vm.objectWillChange.send()
}), secondaryButton: Alert.Button.cancel())
self.presentAlert = true
}
}
}
struct TaskListView_Previews: PreviewProvider {
static var previews: some View {
TaskListView()
}
}
previewAware()
is just a method that decides wether to pass the built-in preview
or shared
static func previewAware() -> PersistenceController{
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
return PersistenceController.preview
}else{
return PersistenceController.shared
}
}