UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: How to properly pass a Core Data Element from a view to a sub-view? How to reload sub-view upon dismiss, without returning to parent-view?

Forums > SwiftUI

Hello,

When using core data, could you please tell me what is the correct way to pass a FetchedResult Element to another view, for editing?

My issue is that when I @FetchRequest and pass a specific fetched element to the next view, the subview I pass the element to reloads after I return to the Main View.

Here's what I've got:

A Contract element and an Invoice element in a one-to-many relationship. One contract has multiple invoices assigned to it.

In the Content View, I fetch all the contract elements, and loop through them to create a list of contracts. Each row shows one specific contract, and is a NavigationLink to a detailed view of that contract. The detailed view shows attributes of the contract.

I only perform a @FetchRequest in the Content View, and not in the sub-views.

import SwiftUI

struct ContentView: View {

    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: []) var contracts: FetchedResults<Contract>

    var body: some View {
        NavigationStack {
            List {
                ForEach (contracts, id: \.self) {contract in

                    NavigationLink {
                        DetailView(contract: contract)
                    } label: {
                        ContractListRow(contract: contract)
                    }

                }.onDelete(perform: deleteContract)
            }
            [...]

The navigationLink to the DetailContract view takes a FetchedResults<Contract>.Element as a parameter. In the detailed view:

struct DetailView: View {

  @Environment(\.managedObjectContext) var moc
  @State var contract: FetchedResults<Contract>.Element

  var body: some View {
  List {
      //*list containing attributes of the element contract*
      //*a NavigationLink row that takes you to a list of invoices*

           Section {
                NavigationLink {
                       InvoiceList(contract: contract)
                    } label: {
                        Label("To Invoices Section", systemImage: "arrow.right.circle")
                            .foregroundColor(.white)
                    }
                    .listRowBackground(Color.blue)

                }
   }

Each individual Contract detail view also has a NavigationLink which takes me to another view, in which I show a list of invoices. Each row is an invoice signed in relation to that specific contract.

@Environment(\.managedObjectContext) var moc
@Environment(\.dismiss) var dismiss

@State var contract: FetchedResults<Contract>.Element

var body: some View {
    NavigationStack {
        List {
            ForEach(contract.invoiceArray, id:\.self) { invoice in
                InvoiceListRow(invoice: invoice)
            }.onDelete(perform: deleteInvoice)

        }

I add an invoice via a sheet I show on the Invoice list view. I enter the attributes inside the form, but when I click "save" and dismiss the sheet, the new invoice does not appear.

I return to contract detail view and back to the invoice list view, and the invoice appears. The only workaround I found is dismissing both the add invoice sheet and the invoice list view at the same time when I click "save".

I am not sure what I am doing wrong, but it may have to do with the fact that I perform a fetchRequest only in the content view or with the fact that the app is reloading only once I return to detail view.

The problem persists if I create an .onTapGesture for each DetailView row, so that I may edit in a sheet each attribute of the element contract. If I change a row in the Detail View, the change takes effect when I return to Content View and back to Detail View.

I do not have this issue when my file has only one element, with a ton of attributes. But using one single element works up to a point.

Another solution would be...to perform a fetch request of all Contract elements within the DetailView or the InvoiceList view. I could fetch a filtered list of contracts...and filter the contracts using an NSPredicate, but the NSPredicate should be a contract.attribute of the contract I'm passing...which will not work, as I will get an initialization error.

@State var contract: FetchedResults<Contract>.Element    //I pass here a Contract from the previous view

@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "registrationNumber == %@", contract.registrationNumber)) var contracts: FetchedResults<Contract>    //I perform a fetch request depending on the registrationNumber attribute of the  Contract I passed from the previous view

I've seen Paul's video on dynamic filtering, but he's using a hardcoded string (a default string value) to filter, and not an open variable (such as @State var filter: String).

Please, help :D. And please be gentle with my coding skills, as I am still learning core data. What am I doing wrong?

Kind regards,

Andrei

2      

Ok, so I guess a long and complicated question sometimes has a simple answer...

Just pass the Entity to the next view, and wrap the entity under an ObservedObject wrapper on the next view.

So in the child view in the example above, it should be @ObservedObject var contract: Contract .

Now I see why Mr Paul Hudson creates a @StateObject private var dataController = DataController() in the App view and injects it into the environment.

Hope this helps someone!

2      

hi,

as @andrei912 points out, the items that come from Core Data (Contract, Invoice) should be marked as @ObservedObject and not @State. the latter works well with structs, the former works well with objects (classes) that are ObservableObjects (and all Core Data object are @ObservableObjects).

one other thing: passing along a Contract as an argument to the DetailView works when marked with @ObservedObject, and you can still display a List with the ForEach with the contract.invoiceArray (i assume this is a computed property to turn the underlying NSSet into an array) as long as you remember this point:

  • changing an Invoice property (or adding/removing and Invoice) only changes the underlying NSSet for the Contract; no value of the Contract itself is changed.

therefore, if you change anything involving an Invoice, please be sure to add this line before you make a change (including an insertion or deletion):

invoice.contract.objectWillChange.send() // assuming invoice.contract is the associated Contract
// now edit, add, or delete the invoice

this tells the associated Contract that it has effectively changed; your DetailView will get the message to redraw because it holds a reference to the Contract as an @ObservedObject.

(so, there's no need to have an elaborate @FetchRequest in your DetailView.)

as with @andrei912 ... hope that helps,

DMG

2      

@delawaremathguy

Thank you! ObservedObject and .objectWillChange.send() did the trick for me.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.