Core Data list relationship of Parent Object - ios

i have two Core-Data entities. One called "Program" and one called "Exercise". Program has a one-to-many relationship to Exercise.
I add Exercises to a Program in another view.
I have one view with all the programs listed and a navigation link which leads to this view.
In this view I want to list all the exercises I assigned earlier to the program. However if I try this code, I get the error message: Generic struct 'ForEach' requires that 'Exercise' conform to 'RandomAccessCollection'.
I am new to Core Data Relationships and think I might have forgotten to declare something in the CoreDataClass File. The Problem is that I cant find any other solutions to this question.
struct ProgramView: View {
#ObservedObject var ProgramDetail: Program
var body: some View {
List {
ForEach(ProgramDetail.exercise!, id: \.self) { exercise in
Text(exercise.name!)
}
}
This generates the List of all Programs:
struct ProgramList: View {
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Program.date, ascending: false)]) var programFetch: FetchedResults<Program>
var body: some View {
NavigationView {
List {
ForEach(programFetch, id: \.self) { program in
NavigationLink(destination: ProgramView(ProgramDetail: program)) {
Text(program.title!)
}
}
}.navigationBarItems(
trailing:
NavigationLink(destination: CreateProgram()) {
Image(systemName: "plus.circle").font(.system(size: 25))
})
}
}
Here is a screenshot of the Interface?:
Interface

The error message is explaining that you must have a Random Access Collection, so that ForEach can iterate through the collection.
For example, Swift's Array conforms to RandomAccessCollection protocol, so you could use an array.
I'd suggest that you read up on collections, either in the Swift documentation or on Apple's Developer website.
There are probably a few ways to achieve a solution, but perhaps the easiest method is as follows...
struct ProgramView: View {
#ObservedObject var programDetail: Program
#FetchRequest(entity: Exercise.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Exercise.name, ascending: true)],
) var allExercises: FetchedResults<Exercise>
var body: some View {
List {
// in this line you create an array of fetched results
// filtered to contain only exercises for programDetail
let exercisesForProgram = allExercises.filter { $0.program == programDetail }
ForEach(exercisesForProgram) { exercise in
Text(exercise.name!)
}
}
}
}

Assuming you haven't changed the Arrangement of your To Many relationship to Ordered, ProgramDetail.exercise is an NSSet?.
You could define a computed property to transform it to an Array of Exercise:
extension Program {
var exerciseArray: [Exercise] {
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
return exercise?.sortedArray(using: [sortDescriptor]) as? [Exercise] ?? [Exercise]()
}
}
In ProgramView now you could then create your List using the Array as follows:
List {
ForEach(programDetail.exerciseArray, id: \.self) { exercise in
Text("\(exercise.name ?? "")")
}
}
In case exercise is an Ordered relationship, then ProgramDetail.exercise will be an NSOrderedSet?.
In this case the definition of exerciseArray is even simpler:
var exerciseArray: [Exercise] {
return exercise?.array as? [Exercise] ?? [Exercise]()
}

Related

Problem with passing entity between views (CoreData)

I have two views: List of trips and detail view.
There is my coreData entity
There is list if cities:
struct TripView: View {
#EnvironmentObject var viewRouter: ViewRouter
#FetchRequest(entity: Trip.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Trip.startDate, ascending: true)], predicate: nil, animation: .linear) var trips: FetchedResults<Trip>
#State var tappedTrip = 0
var body: some View {
VStack(spacing: 20) {
ForEach(trips, id: \.self) { trip in
TripRow(trip: trip)
.environmentObject(viewRouter)
}
}
}
struct TripRow: View {
#EnvironmentObject var viewRouter: ViewRouter
var trip: Trip
var body: some View {
...
.fullScreenCover(isPresented: $viewRouter.isShowingDetailTripView) {
DetailTripView(trip: trip)
.environmentObject(viewRouter)
}
.onTapGesture {
viewRouter.isShowingDetailTripView.toggle()
}
And detail view:
struct DetailTripView: View {
var trip: Trip
var body: some View {...}
But when I tap on any city in detail view every time I have the first one
For example "Берлингтон"
How I can get the corresponding value of Trip in detail view?
Tried to debug but without any results :c
Each of your rows is monitoring $viewRouter.isShowingDetailTripView for presentation of a fullScreenCover - so when that boolean value toggles, in theory they could all be responding to it. In practice your first row is doing the job.
To correctly represent your state in the view router, it doesn't only need to know whether to show a full screen trip - it also needs to know which full screen trip to display.
The usual way is to use an Optional object in place of the boolean.
class ViewRouter: ObservableObject {
// ...
#Published var detailTrip: Trip?
}
When this optional value is nil, the full screen won't display.
In your row, change your action to set this value:
// in TipRow
.onTapGesture {
viewRouter.detailTrip = trip
}
In terms of the modifier to display the full screen, I wouldn't put that in the row itself, but move it up to the list, so that you only have the one check on that value.
// in TipView
VStack {
ForEach(...) {
}
}
.fullScreenCover(item: $viewRouter.detailTrip) { trip in
DetailTripView(trip: trip)
}
This may cause an issue if you want to persist ViewRouter between sessions for state preservation, as it'll now contain a Trip object that it doesn't own. But this approach should get you much closer to how you want your UI to work.

SwiftUI Core Data ObjectWillChange with a computed var in Entity

I have an app that stores Person and MoneyEntries entities in a Core Data DB.
Person has 1:many relations to MoneyEntry. I created a computed var in an extension of Person that calculates the sum of all MoneyEntry entries related to this very person:
extension Person {
var sumOfEntries: Double {
var sum = 0.0
entries.forEach({sum += $0.value})
return sum
}
}
In a first view I list all Person objects and the sum of their corresponding MoneyEntries:
struct PeopleListView: View {
#Environment(\.managedObjectContext) var viewContext
#FetchRequest var people: FetchedResults<Person>
init() {
let request = NSFetchRequest<Person>(entityName: "Person")
request.sortDescriptors = [NSSortDescriptor(key: "lastName_", ascending: true)]
let predicate = NSPredicate(format: "TRUEPREDICATE")
request.predicate = predicate
_people = FetchRequest(fetchRequest: request)
}
var body: some View{
ForEach(people, id: \.self) { person in
Text("\(String(person.sumOfEntries) €")
}
}
}
In a second view I add a MoneyEntry object:
let entryObj = MoneyEntry(context: viewContext)
entryObj.person = person
entryObj.value = valueAsDouble
person.objectWillChange.send()
try? viewContext.save()
But in my first view the sumOfEntries does not update. What can I do to fix this?
Well, In answer to your original question, Core Data Entities conform to ObservableObject so you could do this in your computed property:
extension Person {
var sumOfEntries: Double {
var sum = 0.0
entries.forEach({sum += $0.value})
objectWillChange.send() // Put it here
return sum
}
}
However, why would you? You could simply substitute
var body: some View{
ForEach(people, id: \.self) { person in
Text("\(person.reduce(0) {$0.entries.value, +}.description €")
}
}
for:
var body: some View{
ForEach(people, id: \.self) { person in
Text("\(String(person.sumOfEntries) €")
}
}
and compute it on the spot. It will always be current for the Person entity you are referencing without resorting to an objectWillChange publisher.

ForEach NavigationLink with State and Binding

I'm trying to generate a ForEach with a NavigationLink and use State and Binding to pass some entity around:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(
entity: MyEntity.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \MyEntity.name, ascending: true)
]
) var entries: FetchedResults<MyEntity>
var body: some View {
NavigationView {
List {
ForEach(entries, id: \.self) { (entry: MyEntity) in
NavigationLink(destination: DetailView(myEntry: $entry)) {
Text(entry.name!)
}
}.
}
}
}
}
And then the following view:
struct DetailView: View {
#Binding var myEntry: MyEntity
var body: some View {
Text(myEntry.name!)
}
}
The problem is I can not pass the value to Detail view since the error:
Use of unresolved identifier '$entry'
What is wrong here and how to solve this?
If I just have a simple #State its no problem to pass it via the binding, but I want/need to use it in the ForEach for the FetchedResults
EDIT: If I remove the $ I get Cannot convert value of type 'MyEntity' to expected argument type 'Binding<MyEntity>'
EDIT2: The purpose is to pass some object to DetailView and then pass it back later to ContentView
Use the following in ForEach
ForEach(entries, id: \.self) { entry in
NavigationLink(destination: DetailView(myEntry: entry)) {
Text(entry.name!)
}
}
and the following in DetailView
struct DetailView: View {
var myEntry: MyEntity
var body: some View {
Text(myEntry.name!)
}
}
The MyEntity is class, so you pass object by reference and changing it in DetailView you change same object.
FetchedResults<MyEntity> entities does not conform to Binding or DynamicProperty. For the $ symbol to workout must conform to Binding.
You can make a FetchedResultsController in an ObservableObject to get the same functionality as an #FetchRequestand have the ability to pass the values in an EnvironmentObject or ObservedObject.

SwiftUI: #FetchRequest For Core Data is not Working

I am using a simple coredata model and I have an Entity that is called Movements and it has 4 fields, date, category, description, amount.
This the view:
struct TransactionsView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Movements.entity(), sortDescriptors: []) var transactions:FetchedResults<Movements>
#State private var showAddModal = false;
var body: some View {
VStack {
List {
ForEach(transactions, id: \.id) { t in
Text(t.desc ?? "No Cat")
}
}
}
}
}
struct TransactionsView_Previews: PreviewProvider {
static var previews: some View {
return TransactionsView()
}
}
I used the same code for another model, categories, and it worked. For some reason here the previ keeps crashing with a generic error, I have no idea that says basically that my app has crashed.
I thought that maybe Transaction could be a reserved word and renamed to Movements but still the same error.
Is there also a way to debug the #FetchRequest to see the data returned?

Unable to infer complex closure return type SwiftUI ForEach

I'm trying to show a filtered List which contains just subjects that match the exact self.day[index]. However, when I try to use the if clause for this, I get the error Unable to infer complex closure return type; add explicit type to disambiguate. Can somebody find out any other way to filter subject by subject.day to be equal to self.days[index] please? Here's my code, thank you:
import SwiftUI
import CoreData
struct ProfileView: View {
#Environment(\.managedObjectContext) var moc: NSManagedObjectContext
#FetchRequest(
entity: Subject.entity(),
sortDescriptors:[
//NSSortDescriptor(keyPath: \Subject.day, ascending: true),
NSSortDescriptor(keyPath: \Subject.start, ascending: true)
]
) var subjects: FetchedResults<Subject>
#State private var showAddScreen = false
#State private var name: String = ""
#State private var surname: String = ""
let days = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok"]
var body: some View {
Form {
ForEach(0 ..< days.count) { index in
Section {
Text(self.days[index])
List {
ForEach(self.subjects, id: \.self) { subject in //here the { shows an error, If I remove the if clause, it works, but obviously I don't have subjects filltered, which is what I need
if subject.day == self.days[index] {
SubjectCell(name: "\(subject.name!)",
place: "\(subject.place!)",
start: self.formatTime(date: subject.start ?? Date()),
end: self.formatTime(date: subject.end ?? Date()),
type: subject.type,
occurance: subject.occurance)
}
}
.onDelete(perform: self.deleteSubject)
Wrappping whole if {..} into Group {..} solved the problem
Would not it be better to filter out subjects based on the same criteria, then return a list of them. There are opportunities to do it in FetchRequest or inside List {}, then use return ForEach(filteredSubjects, id: \.self) {....

Resources