How to use ManagedObjectContext in non View Swift file - core-data

I use standard approach for injection of CoreData ManagedObjectContect from App struct
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
to all my views via wrapper
#Environment(\.managedObjectContext) private var viewContext
and it works fine.
I'd like to create class with methods handling CoreData objects in the same context but #Environment wrapper used as above doesn't work in non SfiftUI file.
What is recommended approach here to handle CoreData?

Related

SwiftUI: Widgets and Core Data

I'm trying to create a widget that makes use of Core Data, and in the widget extension, I can't import the project module:
import WidgetKit
import SwiftUI
import Intents
import MyProjectModule
import CoreData
...
...
...
struct CrossFitLoggerWidgetEntryView : View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(fetchRequest: NSFetchRequest(entityName: "Exercise"), animation: .default)
private var fetchedResults: FetchedResults
var entry: Provider.Entry
var body: some View {
ForEach(fetchedResults, id: \.self) { (loggedExercise: Exercise) in
Text(loggedExercise.exerciseName)
}
}
}
The Exercise class is the core data automatically generated class, which I guess should be imported in the widget extension via my project so I can make use of it?
Is there any step I'm missing? The xcdatamodel file is already added to the widget extension target and I have already created an AppGroup.
Thanks in advance!

SwiftUI: updating View with changes to Core Data relationship array

I have a SwiftUI view where a Core Data NSManagedObject is passed in (without #FetchRequest) and where the subview needs to show the one-to-many relationship from the first entity. In this case, a Person is passed in, and I want to show the Tags associated with that Person.
struct CJMapsCalloutView: View {
#ObservedObject var personAddress: PersonAddress
var body: some View {
VStack(alignment: .leading, spacing: 8.0) {
if let personTags = personAddress.person?.getSortedListOfAssignedTags() {
ContactTagsList(personTags: personTags)
}
}
}
The ContactTagsList
struct ContactTagsList: View {
#State var personTags: [Tag]
var body: some View {
HStack(spacing: 0.0) {
ForEach(personTags) { (tag: Tag) in
ContactTag(personTag: tag)
}
.padding(.leading, 0).padding(.trailing, 2)
}
}
When using #State, it displays the tags list just fine, and even changes to individual Tags are reflected automatically in the view as well. But any changes to the 'relationship', whether adding or removing tags from the Person, don't cause the view to update.
I tried using #ObservedObject instead of #State for the var personTags, but that gives errors at compile time:
Property type '[Tag]' does not match that of the 'wrappedValue'
property of its wrapper type 'ObservedObject'
I'm new to SwiftUI, so I'm not sure what the issue is with using #ObservedObject. And if I can't use that, what's the best way to handle this situation to get automatic updates to the relationship?

Core data in SwiftUI

Trying to implement core data in SwiftUI, I've run into a wall.
Following many tutorials, I wrote the following project, presumably exactly as instructed but the app won't build.
I'm stuck at a very early stage, so I'm hoping someone can help here.
I created 2 entities in my XCDtatamodel, each with a string property called "airportName"
I simply try to display a list of one of the entities :
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Takeoffs.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Takeoffs.eventDate, ascending: false)]) var fetchedTakeoffs: FetchedResults<Takeoffs>
var body: some View {
List {
ForEach (fetchedTakeoffs, id: \.self) { item in
Text(item.airportName) // THIS IS WHERE I GET THE ERROR
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return ContentView().environment(\.managedObjectContext, context)
}
}
But Xcode tells me that "Value of type 'NSManagedObject' has no member 'airportName'"
It's like my XCDatamodel is not connected to the app.
I created the project by checking the SwiftUI, Use CoreData checkboxes.
The whole code can be found here :
https://github.com/Esowes/RecentExp
Thanks for any pointers.
I am able to build it. But not sure it would run as you expected it to. Please go through. I think properties of the takeOff items are optional you were getting error.
List {
ForEach (fetchedTakeoffs, id: \.self) { item in
Text(item.airportName ?? "")
}
}

SwiftUI & Core Data - How do I use a parent record in a predicate?

Is it possible to use a core data record in a predicate inside the #FetchRequest property wrapper in SwiftUI?
I have a list of Project and a list of Tasks. I want to tap on a project and navigate to a list of related tasks for that project. I can't seem to find a way to pass in the parent project in a way that SwiftUI can see before the #FetcheRequest is initialized.
I tried placing the parent project in an EnvironmentObject. This is called when I navigate from the ProjectListView to the TaskListView.
TaskListView()
.environment(\.managedObjectContext, self.managedObjectContext)
.environmentObject(self.projectToEdit)
Then in the TaskListView I added tried this:
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var parentProject: Project
#FetchRequest(
entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.name, ascending: true)
],
predicate: NSPredicate(format: String(format: "%#%#", "taskProject", " == %#"), parentProject)
) var tasks: FetchedResults<Task>
I get the following error on the line with the predicate.
Cannot use instance member 'parentProject' within property initializer; property initializers run before 'self' is available
So is there a way to write a predicate in some way that can use the parent project? Passing the project to the task view does not seem like it's going to work. How else would I go about using a record in a predicate like this?
The FetchRequest can be dynamically created in the init method. That way you can vary predicate and sort conditions. Here is some sample code to achieve that.
// sample Project class
class Project:NSManagedObject {
var id : String
var name : String
}
// sample Task class
class Task:NSManagedObject {
var id : String
var prjId : String
var name : String
}
// Task List View
struct TaskListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
private var tasksRequest: FetchRequest<Task>
private var tasks: FetchedResults<Task> { tasksRequest.wrappedValue }
private var project:Project
// init Task with Project
init(_ project:Project) {
self.project = project
// create FetchRequest
self.tasksRequest = FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(key: "name", ascending:true)],
predicate: NSPredicate(format: "prjId == %#", project.id))
}
var body: some View {
VStack {
Section(header: Text("Tasks under \(project.name):")) {
// access the fetched objects
ForEach(tasks, id:\.id) { task in
Text("\(task.name)")
}
}
}
}
}
Then the call to TaskListView() would look like:
// call to TaskListView
TaskListView(self.projectToEdit)
.environment(\.managedObjectContext, self.managedObjectContext)

SwiftUI - Use #Binding with Core Data NSManagedObject?

I'm trying to make an edit form that can take a value as a #Binding, edit it, and commit the change. In this case, I'm populating a list with Core Data records using the #FetchRequest property wrapper. I want to tap on a row to navigate from the List to a Detail view, then on the Detail view I want to navigate to the Edit view.
I tried doing this without the #Binding and the code will compile but when I make an edit, it is not reflected on the previous screens. It seems like I need to use #Binding but I can't figure out a way to get a NSManagedObject instance inside of a List or ForEach, and pass it to a view that can use it as a #Binding.
List View
struct TimelineListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
// The Timeline class has an `allTimelinesFetchRequest` function that can be used here
#FetchRequest(fetchRequest: Timeline.allTimelinesFetchRequest()) var timelines: FetchedResults<Timeline>
#State var openAddModalSheet = false
var body: some View {
return NavigationView {
VStack {
List {
Section(header:
Text("Lists")
) {
ForEach(self.timelines) { timeline in
// ✳️ How to I use the timeline in this list as a #Binding?
NavigationLink(destination: TimelineDetailView(timeline: $timeline)) {
TimelineCell(timeline: timeline)
}
}
}
.font(.headline)
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Lists"), displayMode: .inline)
}
} // End Body
}
Detail View
struct TimelineDetailView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Binding var timeline: Timeline
var body: some View {
List {
Section {
NavigationLink(destination: TimelineEditView(timeline: $timeline)) {
TimelineCell(timeline: timeline)
}
}
Section {
Text("Event data here")
Text("Another event here")
Text("A third event here")
}
}.listStyle(GroupedListStyle())
}
}
Edit View
struct TimelineEditView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var newDataValue = ""
#Binding var timeline: Timeline
var body: some View {
return VStack {
TextField("Data to edit", text: self.$newDataValue)
.shadow(color: .secondary, radius: 1, x: 0, y: 0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear {
self.newDataValue = self.timeline.name ?? ""
}.padding()
Spacer()
}
.navigationBarItems(
leading:
Button(action: ({
// Dismiss the modal sheet
self.newDataValue = ""
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Cancel")
},
trailing: Button(action: ({
self.timeline.name = self.newDataValue
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
// Dismiss the modal sheet
self.newDataValue = ""
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Done")
}
)
}
}
I should mention, the only reason I'm even trying to do this is because the modal .sheet() stuff is super buggy.
To implement creation and editing functionality with Core Data it is best to use nested managed object contexts. If we inject a child managed object context, derived from the main view context, as well as the managed object being created or edited that is associated with a child context, we get a safe space where we can make changes and discard them if needed without altering the context that drives our UI.
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = viewContext
let childItem = childContext.object(with: objectID) as! Item
return ItemHost(item: childItem)
.environment(\.managedObjectContext, childContext)
Once we are done with our changes, we just need to save the child context and the changes will be pushed up to the main view context and can be saved right away or later, depending on your architecture. If we are unhappy with our changes we can discard them by calling rollback() on our child context.
childContext.rollback()
Regarding the question of binding managed objects with SwiftUI views, once we have our child object injected into our edit form, we can bind its properties directly to SwiftUI controls. This is possible since NSManagedObject class conforms to ObservableObject protocol. All we have to do is mark a property that holds a reference to our child object with #ObservedObject and we get its bindings. The only complication here is that there are often type mismatches. For example, managed objects store strings as String?, but TextField expects String. To go around that we can use Binding’s initializer init?(_ base: Binding<Value?>).
We can now use our bindings, provided that the name attribute has a default empty string set in the managed object model, or else this will crash.
TextField("Title", text: Binding($item.title)!)
This way we can cleanly implement the SwiftUI philosophy of no shared state. I have provided a sample project for further reference.
#Binding only works with structs.
But CoreData result are Objects (NSManagedObject adopting ObservableObject). You need to use #ObservedObject to register for changes.

Resources