SwiftUI: Widgets and Core Data - 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!

Related

How to use ManagedObjectContext in non View Swift file

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?

Converting Key-Value Observation in AppKit/UIKit to Combine and SwiftUI

I'm having trouble wrapping my head around how to use Combine in SwiftUI. I'm accustomed to using key-value observation in AppKit an UIKit because view controllers don't need to know about each other and can just react to some global objects that help determine state.
For example, in an AppKit/UIKit app, I would create a global state object like this:
//Global State file
#objc class AppState: NSObject {
#objc dynamic var project: Project?
}
//Create an instance I can access anywhere in my app
let app = AppState()
Then in a view controller, I can get notified of any changes to my app-wide project instance and react accordingly:
//View Controller
class MyViewController: NSViewController{
var observerProject: NSKeyValueObservation?
override func viewDidLoad() {
observerProject = app.observe(\.project) { object, change in
self.refreshData()
}
}
func refreshData(){
//Query my persistent store and update my UI
}
}
What is the Combine/SwiftUI analog to this?
Do I need to create a Publisher and then listen to my global object changes? If so, how do I make my Core Data #FetchRequest (whose predicate includes my global Project object) respond in real-time?
I've done things the old way for so long that this transition to SwiftUI/Combine is rather confusing to me. 🙂
#FetchRequest doesn't work well with a dynamic predicate (There are some workarounds in SO) you will have to use the "old school" NSFetchedResultsController for that and put it into an ObservableObject. Here is a video with a sample. It is a lot of setup and code.
import SwiftUI
import Combine
class AppState: ObservableObject {
static let shared = AppState()
#Published var project: String?
//Just to mimic updates
var count = 0
private init() {
//Mimic getting updates to project
Timer.scheduledTimer(withTimeInterval: 2, repeats: true){ timer in
print("Timer")
self.project = self.count.description
self.count += 1
if self.count >= 15{
timer.invalidate()
}
}
}
}
class MyViewModel: ObservableObject{
#Published var refreshedData: String = "init"
let appState: AppState = AppState.shared
var projectCancellable: AnyCancellable?
init() {
projectCancellable = appState.$project.sink(receiveValue: {
value in
print(value ?? "nil")
self.refreshData()
})
}
func refreshData() {
refreshedData = Int.random(in: 0...100).description
}
}
struct MyView: View {
#StateObject var vm: MyViewModel = MyViewModel()
var body: some View {
VStack{
Text(vm.refreshedData)
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}

CoreData bug in Xcode 12.1

I am working through Paul Hudson's 100 Days of SwiftUI and on Project 11 have hit a frustrating issue with CoreData. This is a direct lift of Paul's code that compiles and runs fine in his video. The Bookworm.xcdatamodeld has a single entity named Student that has two attributes: a UUID named id and a String named name.
It compiles fine, but running it results in a crash on the ForEach, with 'students' underlined in red. The error message that pops up in the console says:
2020-10-31 12:13:47.934507-0400 Bookworm[614:7766183] [error] error: No NSEntityDescriptions in any model claim the NSManagedObject subclass 'Bookworm.Student' so +entity is confused. Have you loaded your NSManagedObjectModel yet ?
CoreData: error: No NSEntityDescriptions in any model claim the NSManagedObject subclass 'Bookworm.Student' so +entity is confused. Have you loaded your NSManagedObjectModel yet ?
2020-10-31 12:13:47.934651-0400 Bookworm[614:7766183] [error] error: +[Bookworm.Student entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
CoreData: error: +[Bookworm.Student entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
2020-10-31 12:13:47.953419-0400 Bookworm[614:7766183] [SwiftUI] Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x6000008d0820>
I have searched a ton, and tried every recommended solution that I have found including: simply closing and reopening Xcode (Step 1), cleaning the project and then repeating Step 1, and deleting all the derived data and repeating Step 1. I have verified that Current Product Module is selected in the inspector for the Module, and that Codegen has Class Definition selected.
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>
var body: some View {
VStack {
List {
ForEach(students, id: \.id) { student in
Text(student.name ?? "Unknown")
}
}
}
}
}
If you are using SwiftUI lifecycle, you should initialize NSPersistentContainer in a parent View (or App) and import managedObjectContext to the environment.
In your case, it could be something like this:
import SwiftUI
import CoreData
#main
struct coreDataParadigmApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
struct ContentView: View {
#FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>
var body: some View {
VStack {
List {
ForEach(students, id: \.id) { student in
Text(student.name ?? "Unknown")
}
}
}
}
}
// DONT FORGET TO CHANGE THE NAME OF YOUR FILE
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "coreDataNameOfFile")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

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)

Resources