Master-Detail with CoreData in SwiftUI?

I have been trying to get a master detail application working for too long. I've tried many things and nothing really seems to work. Part of my problem is that I don't know the best way to go about it.

I have a core data model created and I can add master items. I have a detail view in a tab view that should display the detail items for the global selected master (optional value on environment object data).

The questions I have are:

  1. What is the best way to do a detail view with a list? Iterate over the master object's list of detail items or use a fetch request with a predicate for the master? The first way kinda seems to work but the list doesn't get updated when items are deleted. It's not clear to me how to do the predicates for the fetch request so I haven't figured that out yet.

  2. When deleting detail items, how is this done? Do you delete from the master and then save? Do you delete the detail items and let core data take care of the master? Is there some other way?

  3. Does anybody know of a decent example of this kind of thing. All the examples I found either don't do editing, don't use core data, etc.

I'm not posting code because I'm trying to figure out best practices and what is recommended by Apple.

Thanks.

Answered by DelawareMathGuy in 715607022

hi,

first, you might start with something simple and straightforward: Paul Hudson's 100 Days of SwiftUI, specifically the BookWorm project which is Day 53 of the curriculum. look for these links:

  • https://www.hackingwithswift.com/100/swiftui
  • https://www.hackingwithswift.com/100/swiftui/53

the only thing missing from your list of requests is the ability to delete an item from within the detail view.

it's exactly because of this omission that i got started on my own project with Xcode 11 called ShoppingList (and then its iterations for ShoppingList14 with iOS 14 and Xcode 12, and now ShoppingList15 with iOS 15 and Xcode 13) that implemented (!) deletion from within the detail view. i've tried to comment extensively throughout the code to indicate issues i found in development, especially those that involve my eventual solution of many "did not update" issues most people run into.

you can find my current development at the following link, and as long as you are on the master branch (based on using @FetchRequests), you should find plenty of code to work with. (the MVVM branch in that project is not quite there yet, which does not use @FetchRequests.)

in answer to your question

What is the best way to do a detail view with a list? Iterate over the master object's list of detail items or use a fetch request with a predicate for the master?

i think you are better off "passing in" a Core Data object to a detail view (often as an @ObservedObject, despite the fact that in-detail-view deletion has a little bit of a twist to it) than making a fetch request.

hope that helps,

DMG

Hi, I too have had problems with the master/detail view of Swift and SwiftUI. But the below video has helped me a great deal.

https://youtu.be/nA6Jo6YnL9g

Hope this helps.

Accepted Answer

hi,

first, you might start with something simple and straightforward: Paul Hudson's 100 Days of SwiftUI, specifically the BookWorm project which is Day 53 of the curriculum. look for these links:

  • https://www.hackingwithswift.com/100/swiftui
  • https://www.hackingwithswift.com/100/swiftui/53

the only thing missing from your list of requests is the ability to delete an item from within the detail view.

it's exactly because of this omission that i got started on my own project with Xcode 11 called ShoppingList (and then its iterations for ShoppingList14 with iOS 14 and Xcode 12, and now ShoppingList15 with iOS 15 and Xcode 13) that implemented (!) deletion from within the detail view. i've tried to comment extensively throughout the code to indicate issues i found in development, especially those that involve my eventual solution of many "did not update" issues most people run into.

you can find my current development at the following link, and as long as you are on the master branch (based on using @FetchRequests), you should find plenty of code to work with. (the MVVM branch in that project is not quite there yet, which does not use @FetchRequests.)

in answer to your question

What is the best way to do a detail view with a list? Iterate over the master object's list of detail items or use a fetch request with a predicate for the master?

i think you are better off "passing in" a Core Data object to a detail view (often as an @ObservedObject, despite the fact that in-detail-view deletion has a little bit of a twist to it) than making a fetch request.

hope that helps,

DMG

I had similar problems with an expenses management app, where I have a hierarchy of Groupings (master) with Transactions (detail) against the lowest Grouping in the hierarchy.  The app uses CoreData with CloudKit sync and is multi-platform., both of which add complexity to the design approach.

My solution is this:

In the CoreData model there are two entities, Grouping and Transaction: with a one-to-many relationship from Grouping to Transaction and one-to-one reciprocal from Transaction to Grouping.  A Fetch of a Grouping (master), or set of Groupings, therefore has the transactions (detail) as an NSManaged NSSet on each Grouping (master) record.  In the Xcode editor of the CoreData model I use Manual Codegen to put the NSManagedObject entity, properties and accessors in my project.  This allows for easier coding of extensions to create computed properties on the CoreData entities, e.g. summing transactions for a Grouping.

I have a DataStore ObservableObject class singleton that handles all CoreData / CloudKit setup and data processing, e.g. all Fetches, insertions, deletions etc.  For example, there is a DataStore method to fetch all Groupings (master records) “getAllGroupings” which returns an array of CoreData Grouping objects, each of which has its own set of Transaction records (detail).  So, in a SwiftUI View, I have a @ObservedObject var dataStore = DataStore.shared, which means that I can therefore refer to the Groupings array in a List by using List(dataStore.getAllGroupings()) …..  To get the list of Transactions in another (detail) view, I pass the required Grouping (master record) to the detail view as a var, then use List(grouping.transactionsArray) - where “transactionsArray” is a computed property on my Grouping CoreData extension that turns an NSSet into a Swift Array. **** This particular solution doesn't need datastore to be an ObservedObject, but I use that for other reasons in the app.

To delete a transaction in detail view, I call a method on dataStore e.g. deleteTransaction(transaction: Transaction) which does a context.delete(transaction) and then a context save.  This deletes the transaction and its entry in the Grouping’s transaction NSSet (according to the delete rules I set in CoreData).

HOWEVER: this delete method (or add a transaction) does not update the view, because the changes are made to the CoreData context, which is not directly observable by SwiftUI. So, in DataStore I have a Combine property to send a notification public let transactionDeleted = PassthroughSubject<(String), Never>()   then in the “deleteTransaction” method, I use transactionDeleted.send(“txn deleted”) - you can set up the PassthroughSubject to send whatever you want. Either in your Master View or DetailView (depends on your processing - need to experiment), have

.onReceive((dataStore.transactionDeleted), perform: { transaction in
            self.statusTime = Date()
        })

This updates a @State var statusTime which, unfortunately, needs to be used somewhere in the view: this triggers a View refresh and, voila, the deletion no longer appears in the View.

NOTE: I use public for my methods and properties in DataStore because this is a Multiplatform app and DataStore is therefore in a framework. This is also why I use Manual CodeGen in Xcode's CoreData models, i.e. to use in a framework.

I also use the PassthroughSubject in non-CoreData apps where there’s complex data processing, which if ObservedObjects are used can cause unnecessary view refreshes.  In these cases, once all processing has completed I then issue the PassthroughSubject send to update the view.

I hope this helps.  Regards, Michaela

Master-Detail with CoreData in SwiftUI?
 
 
Q