0

I have a TableVIewthat gets populated by a FetchResultsController. Fetched items are properly displayed in their own section but what I want to achieve is to show only once the same type of object but store the number of objects that has been fetched. Example: Entity name: Item, entity attribute: itemId: String, category: String. category is used to sort the fetch and create Tableviewsections. So if I had three cell for the same itemId fetched object I just want to display one cell and keep count of how many they were supposed to be displayed, and display it in a label in the only displayed cell. I'm trying to use itemFetchRequest.propertiesToFetch = ["itemId"]and itemFetchRequest.returnsDistinctResults = truethat should eliminate all duplicates based on itemId attribute of Item entity but I still get more than one cell with the same items. Can you spot why itemFetchController is returning multiples of the same item?

This is the code I came up with so far cellForRowAt:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "statisticsCell", for: indexPath) as! StatisticsTableViewCell
    let productPrice: String!
    cell.idInfoLabel.text = itemFetchedResultController?.object(at: indexPath).itemId!
    cell.nameInfoLabel.text = itemFetchedResultController?.object(at: indexPath).itemName!
    // get items details(image, price, minimum stock quantity) from Product Entity
    let item = itemFetchedResultController?.object(at: indexPath).itemName!
        let productRequest: NSFetchRequest<Product> = Product.fetchRequest()
        productRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
    productRequest.predicate = NSPredicate(format: "name == %@", item!)
        productRequest.fetchLimit = 1
        do {
            let fetch = try context.fetch(productRequest)
            cell.productImageView.image = UIImage(data: (fetch[0].productImage! as Data))
            cell.minimumStockInfoLabel.text = fetch[0].minimumStock
            productPrice = fetch[0].price

            //fetch itemes for amount of single object
            let itemId = itemFetchedResultController?.object(at: indexPath).itemId!

            print(itemId!)
            let itemRequest = NSFetchRequest<Item>(entityName: "Item")
            itemRequest.sortDescriptors = [NSSortDescriptor(key: "itemName", ascending: true)]
            itemRequest.predicate = NSPredicate(format: "date BEGINSWITH %@", dateToFetchMin)
            itemRequest.predicate = NSPredicate(format: "itemId == %@", itemId!)
            do {
                let itemFetch = try context.fetch(itemRequest)
                print(productPrice)
                print(itemFetch, itemFetch.count)
                cell.soldQuantityInfoLabel.text = String(describing: itemFetch.count)
                let amount = Double(productPrice!)! * Double(itemFetch.count)
                cell.salesAmountInfoLabel.text = String(describing: amount)
            } catch  {
                print("Error in fetching sold items for cell: \(error)")
            }
        } catch  {
            print("Error in fetching product for cell: \(error)")
        }
    return cell
}

FetchResultController:

var itemFetchedResultController: NSFetchedResultsController<Item>?

and the fetching function :

func configureItemFetchedResultsController() {
        print("configureItemFetchedResultsController(): started")

        // first sortDescriptor filters the date range:  possibly change date from String to dates in both function and  CoreData and use "(date >= %@) AND (date <= %@)" instead of "BEGINSWITH" in predicate
        let itemFetchRequest = NSFetchRequest<Item>(entityName: "Item")
        itemFetchRequest.sortDescriptors = [NSSortDescriptor(key: "category", ascending: true),NSSortDescriptor(key: "itemId", ascending: true)]
        itemFetchRequest.predicate = NSPredicate(format: "order.user.name == %@", UserDetails.fullName ?? "")
        itemFetchRequest.predicate = NSPredicate(format: "date BEGINSWITH %@", dateToFetchMin)
        itemFetchRequest.propertiesToFetch = ["itemId"]
        itemFetchRequest.returnsDistinctResults = true
        //        itemFetchRequest.propertiesToGroupBy = ["category","itemId","itemName"]
        //        itemFetchRequest.resultType = .dictionaryResultType
        itemFetchedResultController = NSFetchedResultsController(fetchRequest: itemFetchRequest, managedObjectContext: context, sectionNameKeyPath: "category", cacheName: nil)
        do {
            try itemFetchedResultController?.performFetch()
            self.statisticsTableView.reloadData()
            print("configureItemFetchedResultsController(): sold items fetched")
        } catch  {
            //            fatalError("failed to fetch entities: \(error)")
            print("configureItemFetchedResultsController(): failed to fetch Item entities: \(error)")
        }
        self.statisticsTableView.reloadData()
    }

actual TableViewresult :

tableView

UPDATES:

After trying to go the Dictionaryroute and using itemFetchRequest.propertiesToFetch = ["category","itemId","itemName"] and itemFetchRequest.propertiesToGroupBy = ["category","itemId","itemName"] I finally got the fetch result I wanted being only one object per itemId, at cost of not having them properly divided into sections named after the parameter category. I decided then to go back using itemFetchResultsControllerto perform the fetch and I get the same fetched objects so using itemFetchRequest.propertiesToFetch = ["category","itemId","itemName"] and itemFetchRequest.propertiesToGroupBy = ["category","itemId","itemName"] makes now .distinctResults work. My problem now is in cellForRowAt. In version1 I get Thread 1: Fatal error: NSArray element failed to match the Swift Array Element typeon the line let item = itemFetchedResultController!.fetchedObjects![indexPath.row]. Casting it as NSArraydidnt solve it. Any ideas on this?

In version2 instead I get *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSKnownKeysDictionary1 itemName]: unrecognized selector sent to instance 0x60000078a2e0'.

So the new code is:

FetchResultController:

func configureItemFetchedResultsController() {
        print("configureItemFetchedResultsController(): started")
        let itemFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
        itemFetchRequest.sortDescriptors = [NSSortDescriptor(key: "category", ascending: true),NSSortDescriptor(key: "itemId", ascending: true)]
        let user = NSPredicate(format: "order.user.name == %@", UserDetails.fullName ?? "")
        let dateFrom = Conversions.dateConvert(dateString: dateToFetchMin)!
        let dateTo = Conversions.dateConvert(dateString: dateToFetchMax)!
        print(dateFrom)
        let from = NSPredicate(format: "date >= %@", dateFrom as CVarArg)
        let to = NSPredicate(format: "date <= %@", dateTo as CVarArg)
        itemFetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [user,from,to])
        itemFetchRequest.propertiesToFetch = ["category","itemId","itemName"]]
        itemFetchRequest.returnsDistinctResults = true
        itemFetchRequest.propertiesToGroupBy = ["category","itemId","itemName"]
        itemFetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType 

        itemFetchedResultController = NSFetchedResultsController(fetchRequest: itemFetchRequest, managedObjectContext: context, sectionNameKeyPath: "category", cacheName: nil) as? NSFetchedResultsController<Item>
        do {
            try itemFetchedResultController?.performFetch()
            let resultsDict = itemFetchedResultController!.fetchedObjects!
            print(resultsDict as NSArray)
            print("configureItemFetchedResultsController(): sold items fetched")
        } catch  {
            //            fatalError("failed to fetch entities: \(error)")
            print("configureItemFetchedResultsController(): failed to fetch Item entities: \(error)")
        }
        self.statisticsTableView.reloadData()
    }

cellForRowAt version1:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let item = itemFetchedResultController!.fetchedObjects![indexPath.row] //as NSArray
        let name = item.itemName!//["itemName"]!
        let itemId = item.itemId!
//        let productPrice: String!
        let cell = tableView.dequeueReusableCell(withIdentifier: "statisticsCell", for: indexPath) as! StatisticsTableViewCell
        let productRequest: NSFetchRequest<Product> = Product.fetchRequest()
        productRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        productRequest.predicate = NSPredicate(format: "name == %@", name)
        productRequest.fetchLimit = 1
        do {
            let fetch = try context.fetch(productRequest)
            cell.idInfoLabel.text = fetch[0].productId
            cell.nameInfoLabel.text = fetch[0].name
            cell.productImageView.image = UIImage(data: (fetch[0].productImage! as Data))
            cell.minimumStockInfoLabel.text = fetch[0].minimumStock
            let productPrice = fetch[0].price
            //fetch itemes for amount of single object
            let itemRequest = NSFetchRequest<Item>(entityName: "Item")
            itemRequest.sortDescriptors = [NSSortDescriptor(key: "itemName", ascending: true)]
            itemRequest.predicate = NSPredicate(format: "date BEGINSWITH %@", dateToFetchMin)
            itemRequest.predicate = NSPredicate(format: "itemId == %@", itemId)
            do {
                let itemFetch = try context.fetch(itemRequest)
                print(productPrice!)
                print(itemFetch, itemFetch.count)
                cell.soldQuantityInfoLabel.text = String(describing: itemFetch.count)
                let amount = Double(productPrice!)! * Double(itemFetch.count)
                cell.salesAmountInfoLabel.text = String(describing: amount)
            } catch  {
                print("Error in fetching sold items for cell: \(error)")
            }
        } catch  {
            print("Error in fetching product for cell: \(error)")
        }
        return cell
    }

cellForRowAtversion2 :

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "statisticsCell", for: indexPath) as! StatisticsTableViewCell
        let item = itemFetchedResultController?.object(at: indexPath).itemName!
        //        let item = itemResultsArray[indexPath.row]
        let productRequest: NSFetchRequest<Product> = Product.fetchRequest()
        productRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        productRequest.predicate = NSPredicate(format: "name == %@", item!)
        productRequest.fetchLimit = 1
        do {
            let fetch = try context.fetch(productRequest)
            cell.idInfoLabel.text = fetch[0].productId
            cell.nameInfoLabel.text = fetch[0].name
            cell.productImageView.image = UIImage(data: (fetch[0].productImage! as Data))
            cell.minimumStockInfoLabel.text = fetch[0].minimumStock
            let productPrice = fetch[0].price
            //fetch item for amount of single object
            let itemId = itemFetchedResultController?.object(at: indexPath).itemId!
            let itemRequest = NSFetchRequest<Item>(entityName: "Item")
            itemRequest.sortDescriptors = [NSSortDescriptor(key: "itemName", ascending: true)]
            itemRequest.predicate = NSPredicate(format: "date BEGINSWITH %@", dateToFetchMin)
            itemRequest.predicate = NSPredicate(format: "itemId == %@", itemId!)
            do {
                let itemFetch = try context.fetch(itemRequest)
                print(productPrice!)
                print(itemFetch, itemFetch.count)
                cell.soldQuantityInfoLabel.text = String(describing: itemFetch.count)
                let amount = Double(productPrice!)! * Double(itemFetch.count)
                cell.salesAmountInfoLabel.text = String(describing: amount)
            } catch  {
                print("Error in fetching sold items for cell: \(error)")
            }
        } catch  {
            print("Error in fetching product for cell: \(error)")
        }
        return cell
    }
Vincenzo
  • 5,304
  • 5
  • 38
  • 96
  • I‘m not quite sure why `returnDistinctResults = true` is not giving you distinct results, but it might be irrelevant all together. As you would like to know how many entries there are with a specific `itemId` you might want to have a look at an sql equivalent of grouping and count(*) as described here: https://stackoverflow.com/questions/16917723/nsfetchrequest-for-groupby-and-count-combination#16920548 – FlixMa May 16 '19 at 17:11
  • @KlixxOne I need to have distinct results, as for the number of entries with the specific `itemId`i will perform a fetch in `cellForRowAt`to get it and display it in cell. Now I'm noticing a weird thing: If hard code `dateToFetch` with a value as `"2019-05-16"` `ItemFetchResultController` fetches entities correctly, but if I set it to `""`and then assign it the same value and relaunch `ItemFetchResultController` it won't fetch anything. Any idea of why it is so? – Vincenzo May 16 '19 at 18:55
  • It might be the case that the `lazy` var itemFetchedResultContainer is causing the troubles. It will read the content of the variable `dateToFetch` when it is first accessed. Make sure this usage comes after the correct assignment of dateToFetch. If that’s not possible, another solution is wrapping the definition in a function which creates a new controller each call and sets the dateToFetch to a given parameter. – FlixMa May 16 '19 at 20:48
  • @KlixxOne I also tried that, so I declared it as normal `var`and put its definition inside `configureItemFetchControllerResult()` and call it when I need to perform the fetch, but I get same results of one cell per item fetched. BTW I solved the date issue before this, it was another problem dough. – Vincenzo May 16 '19 at 22:11
  • Could you please give me an example of some data and what you expect to happen? – FlixMa May 17 '19 at 05:35
  • @KlixxOne I updated the code and added a picture of the resulting `Tableview`.My goal is to have only one cell per product, so when `itemFetchResultController`performs the fetch, it should automatically eliminate multiples so I use of `distinctResults = true` but for some reason it doesn't take it into account and keep showing me multiples. I found only another post with the same problem https://stackoverflow.com/questions/46718453/nsfetchrequest-returnsdistinctresults-seems-to-be-ignored but I don't know how to understand if it relates. Run on real iPad but same results. – Vincenzo May 17 '19 at 06:58
  • @KlixxOne I've seen in this post https://stackoverflow.com/questions/24432895/swift-core-data-request-with-distinct-results/29424271 , that `distinctResults` has to be used in conjunction to `.resultType = NSFetchRequestResultType.DictionaryResultType`else it won't work. But then what's the point in using a `FetchResultController` if I than have to extract fetched data into dictionaries and arrays? I should then rewrite `cellForRowAt`to get data from an array instead.. – Vincenzo May 17 '19 at 14:41
  • @KlixxOne I'm almost there. If I fetch directly from context as `let results = try context.fetch(itemFetchRequest)` I am then able to get the result in a dictionary as `let resultsDict = results as! [[String : String]]` from which I populate `TableView` correctly with only one cell per item. The thing I have to solve with this approach is how to create sections and to divide cells into sections based on the parameter `category`, as was done by `FetchResultsController`. Any help on that line of approach? – Vincenzo May 18 '19 at 09:20
  • Instantiate the Controller once in `viewdidload` and execute the request, storing the data in a private variable. Then you could transform that data into a dictionary indexed by your category names. In your tableviewdelegate functions you fetch only the data from that array instead of the database directly. Is that a solution? How many entries do you have? – FlixMa May 19 '19 at 06:45
  • @KlixxOne I found the problem. Declaring the `itemFetchResutlController` as specific entity type when returning results in a dictionary form leads to having `unrecognized selector sent to instance` once I changed that I was the able to get fetched objects values by key subscription. I actually asked another question to be specific on the subject as this one was getting a bit wide. – Vincenzo May 19 '19 at 08:08

1 Answers1

3

After a couple of days of testing different options and getting all sort of errors, I finally corrected the code to work as I wanted. During the process I found out basic points that ( judging by the quantity of posts I had a look at trying to find a solution to my problem ), very a few people got under their belts. This answer is to help others and clarify the mandatory properties and type definitions that are involved in getting distinct results to work.

Step-by-step guide:

1st: itemFetchRequest.returnsDistinctResults = true this set the result to be distinct

2nd: itemFetchRequest.propertiesToFetch = ["category","itemId","itemName"] this is what properties you want to show in your results and they need also to be in .propertiesToGroupBy .

3rd: itemFetchRequest.propertiesToGroupBy = ["category","itemId","itemName"] this is the properties you want distinct results of.

4th: itemFetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType this is the only allowed type to get distinct results.

5th: NSFetchedResultsController<NSDictionary>this is the mandatory type for the controller as otherwise you won't have access to fetched object's parameter's values.

6th: let item = itemFetchedResultController?.object(at: indexPath) this is the mandatory way to get fetched objects. Using fetchedObjects![indexPath.row] gets wrong item. I was getting same two items in both displayed categories.

7th: let itemName = item!["itemName"]! let itemId = item!["itemId"]! this is the way to get parameter's values as the fetched objects are dictionary type.

So the final code for all this is:

Feching:

// dictionary fetch result controller
    func configureItemFetchedResultsController() {
        print("configureItemFetchedResultsController(): started")
        let itemFetchRequest = NSFetchRequest<Item>(entityName: "Item")
        itemFetchRequest.sortDescriptors = [NSSortDescriptor(key: "category", ascending: true),NSSortDescriptor(key: "itemId", ascending: true)]
        // predicates to filter for user and date range:
        let user = NSPredicate(format: "order.user.name == %@", UserDetails.fullName ?? "")
        let dateFrom = Conversions.dateConvert(dateString: dateToFetchMin)!
        let dateTo = Conversions.dateConvert(dateString: dateToFetchMax)!
        print(dateFrom)
        let from = NSPredicate(format: "date >= %@", dateFrom as CVarArg)
        let to = NSPredicate(format: "date <= %@", dateTo as CVarArg)
        itemFetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [user,from,to])
        itemFetchRequest.returnsDistinctResults = true
        itemFetchRequest.propertiesToFetch = ["category","itemId","itemName"]//["category","itemId","itemName"]
        itemFetchRequest.propertiesToGroupBy = ["category","itemId","itemName"]
        itemFetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType //.managedObjectResultType// .dictionaryResultType

        itemFetchedResultController = NSFetchedResultsController(fetchRequest: itemFetchRequest, managedObjectContext: context, sectionNameKeyPath: "category", cacheName: nil) as? NSFetchedResultsController<NSDictionary>// as! NSFetchedResultsController<Item>
        do {
            try itemFetchedResultController?.performFetch()
            print("configureItemFetchedResultsController(): sold items fetched")
        } catch  {
            //            fatalError("failed to fetch entities: \(error)")
            print("configureItemFetchedResultsController(): failed to fetch Item entities: \(error)")
        }
        self.statisticsTableView.reloadData()
    }

Displaying fetched objects:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "statisticsCell", for: indexPath) as! StatisticsTableViewCell
        let item = itemFetchedResultController?.object(at: indexPath)  //fetchedObjects![indexPath.row] gets the wrong item
        print("fetched is: \(String(describing: item))")
        let itemName = item!["itemName"]!
        let itemId = item!["itemId"]!
        let productRequest: NSFetchRequest<Product> = Product.fetchRequest()
        productRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        productRequest.predicate = NSPredicate(format: "name == %@", itemName as! CVarArg)
        productRequest.fetchLimit = 1
        do {
            let fetch = try context.fetch(productRequest)
            cell.idInfoLabel.text = fetch[0].productId
            cell.nameInfoLabel.text = fetch[0].name
            cell.productImageView.image = UIImage(data: (fetch[0].productImage! as Data))
            cell.minimumStockInfoLabel.text = fetch[0].minimumStock
            let productPrice = fetch[0].price
            //fetch itemes for amount of single object
            let itemRequest = NSFetchRequest<Item>(entityName: "Item")
            itemRequest.sortDescriptors = [NSSortDescriptor(key: "itemName", ascending: true)]
            itemRequest.predicate = NSPredicate(format: "date BEGINSWITH %@", dateToFetchMin)
            itemRequest.predicate = NSPredicate(format: "itemId == %@", itemId as! CVarArg)
            do {
                let itemFetch = try context.fetch(itemRequest)
                print(productPrice!)
                print(itemFetch, itemFetch.count)
                cell.soldQuantityInfoLabel.text = String(describing: itemFetch.count)
                let amount = Double(productPrice!)! * Double(itemFetch.count)
                cell.salesAmountInfoLabel.text = String(describing: amount)
            } catch  {
                print("Error in fetching sold items for cell: \(error)")
            }
        } catch  {
            print("Error in fetching product for cell: \(error)")
        }
        return cell
    }

Many thanks for helping me out on this one as well, and this log and detailed answer is to help others understand better the whole process involved. Please comment if there is something wrong in my answer and I'll edit it, so not to mislead others with it.

Vincenzo
  • 5,304
  • 5
  • 38
  • 96