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

SOLVED: Making a view work with two different (but similar) CoreData entities

Forums > SwiftUI

Hi,

I have a CoreData entity with a single string attribute. I created a couple of useful views making lists and a class conforming to ObservableObject with a few @Published vars to pass some data around (including the core data entity object itself). I now find myself with a second CoreData entity with a single string attribute (whoa... deja vu). So I thought, hey, it would be really cool if I could reuse the useful views and maybe just pass some enum over to use to switch between the appropriate source of truth I want to use in my view. I'm a bit lost though on how to do that and I'm not sure what I'm looking for to find a solution.

I know I have an @EnvironmentObject that is going to have to change to another class to pass some data around as they both refer to different entities. I know my @FetchRequest has to change to a different keyPath to point to the appropriate CoreData entity. And I know I need to put the FetchedResults<> into a var to loop through in my list with a ForEach but it needs to be of the appropriate CoreData entity.

I have this feeling that the answer is staring me right in the face, but I'm just not seeing it and not able to come up with what I'm looking for to solve something like this (hard to search for something when you are not quite sure what). Can someone point me in the right direction? I suppose I could duplicate my views and just modify the duplicates to refer to the appropriate CoreData entity and my class for passing data around, but I know there has to be a better way.

What am I missing?

2      

You could use a view model instead of @FetchRequests and then in the view model use NSFetchedResultsController to fetch the appropriate CD entity but vend to the View a single struct type.

So you would have, say, two Core Data entities—Entity1 with a string attribute name and Entity2 with a string attribute title— and a single struct CommonStruct with a string property info. The view model would use two different NSFetchedResultsControllers to pull your entities from CD and then translate them into an @Published variable of [CommonStruct], which your View would read to construct the ForEach. You would probably still want some kind of enum to indicate which entity you are using in case things in the View need to be contextually different.

Not saying this is the best way to do it or even if it should be done (I would say no), but that's one way you could approach it.

3      

You may want to consider using an abstract entity, if it is appropriate for your design, as it is not a usual case.

If you had two entities, say Book and Magazine, you could create and abstract entity - WrittenMaterial

Set WrittenMaterial in the Data Model Inspector and mark it as abstract, and for both Book and Magazine you would choose WrittenMaterial as the parent entity.

You would then do all the fetchRequests for items on WrittenMaterial, and cast as follows

if let book = item as? Book {
   // book code
} else if let magazine = item as? Magazine {
   // magazine code
}

3      

Not saying this is the best way to do it or even if it should be done (I would say no), but that's one way you could approach it.

Thanks for the reply @roosterboy. Well, there is another option that I can see based on your response (bringing things into a common item). I could go with a single entity and add a second Int16 attribute and use 0 and 1 and then filter things based on that. Would that be better or worse? I mean, it seems like it would be far easier for the programming as I would just be dealing with a single entity... but it would also be adding a bunch of 0s and 1s to the database and... should that be done?

2      

@Greenamberred ... Wow! OK... mind blown! I'm going to have to play with this a bit (whether it is ultimately the best way forward or not)... I had no idea I could do this (not surprising as I'm really new to all of this and I know Paul didn't cover this in 100 Days Of SwiftUI)... but this looks really promising as a way to keep these essentially same things in two distinct lists so I can use the appropriate one based on the context I need in the existing views. Thanks.

2      

@Greenamberred I'm not quite understanding this. Continuing the example of Book and Magazine... So In my .xcdatamodeld Book and Magazine both have a name String attribute and also a relationship set up with other entities. I add a WrittenMaterial entity and make it an abstract entity. Do I give it any attributes or relationship? I go to Book and Magazine and set them up as having a parent entity of WrittenMaterial.

I'm also a bit confused about what to do with setting this up:

class PassWrittenMaterialInfo: ObservableObject {
    @Published var name: String
    @Published var selection: WrittenMaterial?

    init(name: String, selection: WrittenMaterial?) {
        self.name = name
        self.selection = selection
    }
}

enum listContext {
    case book, magazine
{
struct MaterialView: View {
    let listContext: listContext
    @Environment(\.presentationMode) var presentationMode
    @Environment(\.managedObjectContext) private var viewContext
    @EnvironmentObject var passWrittenMaterialInfo: PassWrittenMaterialInfo

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \WrittenMaterial.name, ascending: true)],
        animation: .default
    )

    private var materials: FetchedResults<WrittenMaterial>

    var body: some View {
        List {
            ForEach(materials) { item in
                // I'm sorry, but you sort of lost me here as I'm not sure how to make sure I'm only returning a list of either .book or .magazine
                // And actually, Xcode doesn't even get this far as on the FetchRequest it complains:
                // Generic parameter 'Root' could not be inferred
                // Reference to member 'name' cannot be resolved without a contextural type
                // Type 'WrittenMaterial' (aka 'OpaquePointer') does not conform to protocol 'NSFetchRequestResult'

                Button(item.name!) {
                    passWrittenMaterialInfo.name = item.name!
                    passWrittenMaterialInfo.selection = item
                    presentationMode.wrappedValue.dismiss()
                }

And then when calling it, one of these two:

MaterialView(listContext: .book)
MaterialView(listContext: .magazine)

2      

The errors in XCode are because the WrittenMaterials property doesn't have a name attribute.

In the data model definition, the name attribute should be defined on the WrittenMaterials entity, and not on the Book and Magazine entities. Common attributes are defined on the parent not the children.

var body: some View {
    List {
        ForEach(materials) { item in

            if item is Book {
                Text("\(item.name ?? "No book name")")
                // more code here
            } else if item is Magazine {
                Text("\(item.name ?? "No magazine name")")
                // more code here
            }

            // alternative method example
            switch item {
            case is Book:
                Text("Book")
                // more code here
            case is Magazine:
                Text("Magazine")
                // more code here
            default:
                EmptyView()
            }
        }
    }
}

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.