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

Combining similar CoreData entries in a FetchRequest

Forums > SwiftUI

Hello All, I hope someone can help lead me in the right direction. Let me preface this by saying that i am relatively new to Swift, but have a strong programming background. I am looking for way to display entries from a coredata entity in a list view, with like items combined into a sinlge row displaying the combined count as a Quantity.

Example:

In core data, i may have the following 5 entries (first line contains the attributes)

Name / Serial Number / Aquired Date

. sprocket A / 001 / 01/01/2020 . sprocket A / 003 / 01/02/2020 . sprocket A / 005 / 01/02/2020 . sprocket B / 0089 / 01/01/2020 . sprocket B / 0142 / 01/02/2020

I would like my list view to display two row as:

sprocket A -- Qty: 3 sprocket B -- Qty: 2

Can i structure a FetchRequest to combine the entries is such a manner? Or do i need to fetch all records as normal and build a new array with the combined elements to bind my list view to?

Thanks for in advance for the help! I hope i explained it clearly enough.

3      

hi,

i think when you ask CoreData to return entities, they pretty much come back as an array of entities. i don't think you can structure them quite the way you want.

but, there's good news: building a new array that contains a count of each name occurrence is just a few lines of code, but it would be useful to first define a struct to drive your List view:

struct ListItem {
    var name: String
    var quantity: Int
}

in your ContentView, you'll want to have these (here, i am assuming you have some sort of InventoryItem class for the entities you keep in CoreData; and note that i'm opting not to sort these by name -- you'll see below where that can be done).

@FetchRequest(entity: InventoryItem.entity(), sortDescriptors: [])
    var inventoryItems: FetchedResults<InventoryItem> // edited after the fact (i had the wrong type here)
@State private var listItems = [ListItem]()

add an .onAppear() modifier to some view in the ContentView's body property

.onAppear(perform: loadData)

finally, loadData is where you repackage the inventoryItems into listItems. (Note: code edited a few hours after initial post, and, unfortunately, a day later, because i think i originally put the wrong dictionaty type in the code)

func loadData() {
    listItems.removeAll()
        let groupedItems: [String : [InventoryItem]] = Dictionary(grouping: inventoryItems, by: { $0.name! }) // <-- type was edited here
        for key in groupedItems.keys {
            listItems.append(ListItem(name: key, quantity: groupedItems[key]!.count))
        }
}

i think this should work. i've used a similary technique to form sections in a list. you may also now want to sort the listItems, depending on how you want them ordered (it's cheaper to sort this list that to supply a sortDescriptor int the fetchRequest).

hope that helps, DMG

--

3      

hi again,

'been thinking about this overnight, and while i gave a possible answer to your programming question (which i have updated as well), perhaps you would consider redesigning the CoreData setup.

instead of a single entity, perhaps called InventoryItem (with name, serial number, date), you'd consider two entities with a relationship:

  • ItemInstance (with a serial number and date)
  • ItemDescription (with a name)
  • ItemDescription has a one-to-many relationship with ItemInstance, perhaps named instances <--->> description.

then your list has a single fetch for ItemDescriptions and your list elements report the name and the number of instances (instances!.count) for each ItemDescription.

it makes a little bit of sense, i think?

hope that helps, DMG

3      

thanks. This is a good way to look at it. I'll give that a try.

3      

Hi ck

Just adding onto what delaware was saying..... You can work with Core Data directly with SwiftUI. If you have no specific reason to create seperate data model to load core data entities into then you can avoid it.

One good way using Core Data for your needs is as follows -

  • As delaware said it would be best to create 2 entities. One for your ItemDescription and one for your ItemInstance.
  • Make sure your create a relationship in your coredata model. As delaware said a one to many.
  • When you create your ItemDescription entity with a name attribute you want to go to the attributes inspector on the right hand side for that entity.
  • Under constraints click + and you will see this come up: 'comma,separated,properties'
  • Click on this once, wait a sec then click again and change this to 'name' (NB its important that what you put here exactly matches the attribute name that you want to place a constraint on)
  • Go to your SceneDelegate.swift file and add the following:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext // THIS SHOULD ALREADY BE THERE SO DONT ADD

context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // Add this

What is happening here is that you are setting up a constraint in your Core data model to say that you dont want entities saved that have the same name. In your case you dont want a seperate entity saved for everything called 'sprocket a'. By adding the context.mergePolicy you are telling Core data that if a item does have the same name then merge it with whatever entity has the same name. So all your sprocket a's for example will be saved under one entity.

Now when you save each 'sprocket a' you also create a create an ItemInstance (as described by delaware) and add this to the 'sprocket a' array of ItemInstances. The end result you will have only one sprocket a entity saved with an array of ItemInstances. I have created a very quick and basic example for you to give a try -

NB - I have created 2 entities in Core Data Model. One which is called Part which has an attribute 'name' and has a one to many relationship with the other entity called Details which has 2 attributes called 'serialNumber' and 'date' with a many to one relationship with Part. I have put the constraint on the Part entity for its attribute called 'name'. I have just made all attributes for both entities as Strings for ease of explanation but you can change them to suit.

import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(entity: Part.entity(), sortDescriptors: []) var parts: FetchedResults<Part>

    @State private var date = ""
    @State private var name = ""
    @State private var serialNumber = ""

    var body: some View {
        NavigationView {
            TabView {
                Form {
                    Section(header: Text("Part")) {

                            TextField("Name", text: $name)
                            TextField("SerialNumber", text: $serialNumber)
                            TextField("Date", text: $date)
                        Button("Save") { self.save() }

                    }
                }.tabItem {
                    Image(systemName: "house")
                    Text("ADD")
                }

                List {
                    ForEach(parts, id: \.self) { part in
                        NavigationLink(destination: DetailView(part: part)) {
                            Text(part.name ?? "")
                        }
                    }
                }.tabItem {
                    Image(systemName: "list")
                    Text("List")
                }

            }
            .navigationBarTitle("Parts")
        }
    }

    func save() {
        let part = Part(context: self.moc)
        let detail = Details(context: self.moc)
        detail.serialNumber = Int64(serialNumber) ?? 0
        detail.date = date
        part.name = name
        part.addToDetails(detail)
        try? self.moc.save()
        name = ""
        date = ""
        serialNumber = ""
    }
}

//struct ContentView_Previews: PreviewProvider { I have commented the preview out not needed. Just means you have to run in simulator
//    static var previews: some View {
//        ContentView()
//    }
//}

// This view will be displayed from NavLink showing the part name and the details of all the parts just to show you that its merges and has the correct info.
struct DetailView: View {
    let part: Part
    @FetchRequest(entity: Details.entity(), sortDescriptors: []) var details: FetchedResults<Details>
    var detail: [Details] {
        details.filter {
            $0.part!.name == part.name
        }
    }

    var body: some View {
        Form {
            Section(header: Text("Details")) {
                Text(part.name ?? "")
                List {
                    ForEach(detail, id: \.self) { det in
                        HStack {
                            Text("\(det.serialNumber)")
                            Text("\(det.date ?? "JAN")")
                        }
                    }
                }
            }
        }
    }
}

Just create a dummy project, create entities like i have and paste all this and run it. See if it fits what you are after. Let me know if there are any issues.

Dave

3      

Thanks Dave, this is also super helpful. However the specific goal I am trying to reach from all of this is to inlcude, along with the name in that main initial list of parts (what you included on the parts list tab item), the total count of that part from the details entity. I'm really just looking to display a single row with a total count for each part, and then link through to get the detil view.

The obvious solution would be to do a second fetch on the details entity and count each group of the specific part on each itteration of the foreach loop, but to me, that sounds like it would not be the most effecient way considering my details entity could ultimetly have 100k+ entries. Is there a more efficeint way to do this or am i over thinking the problem?

3      

hi,

perhaps i should have provided some code with my "been thinking about this overnight" comment. and yes, i think you are overthinking this.

the main view that provides names and counts only requires a fetch of what i called ItemDescription objects. when you have one of these objects, you can read the number of ItemInstances (each holds a serial number and date acquired) just by referring to the object's instances variable from the Core Data model. something like this:

@FetchRequest(entity: ItemDescription.entity(), sortDescriptors: [])
var descriptions: FetchedResults<ItemDescription>

var body: some View {
  NavigationView {
    List {
      ForEach(descriptions, id: \.self) { description in
        NavigationLink(destination: DetailView(for: description)) {
          Text("name: \(description.name!), quantity: \(description.instances!.count)" )
        }
      }
    }
  }
}

the DetailView then shows a list of the description's instances. there is no second fetching required for that view: it already has a live ItemDescription in front of it, and its instances variable has all the associated ItemInstances.

(ADDED well after this was originally posted): it occurred to me that you might not know how to read the instances of an itemDescription in order to use them as an Array for a List. you can do this with something like the following, assuming you've made ItemInstance conform to Comparable (maybe by comparing dates or serial numbers):

func itemsArray(for description: ItemDescription) -> [ItemInstance] {
  if let items = description.instances as? Set<ItemInstance> { // this should succeed
        return items.sorted(by: <)
    }
    return [ItemInstance]()
}

now you can write the ForEach component of the view as

ForEach(itemsArray(for: itemDescription)) { 
    // ...
}

hope that helps,

DMG

4      

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.