Adam Altinkaya
Adam Altinkaya

Reputation: 639

Dynamic filtering with Swift CoreData

I am developing an application that retrieves data from CoreData. I retrieve a list of items from the database and display these on screen.

The user has an option to filter these items for up to 5 categories in 5 separate drop downs. What is the best way of doing this dynamically? What I mean by that, is if the user selects one filter option, only the items that match that filter will be shown, as well as the other filter options then only showing filter options that exist for the already filtered items.

I hope that makes sense!

This is the code I currently have for retrieving the items:

func showDropDown(filterButton: UIButton) -> Void {
        selectedButton = filterButton
        let popController = UIStoryboard(name: STORYBOARD_NAME,
                                         bundle: nil).instantiateViewController(withIdentifier: STORYBOARD_ID) as! FilterDropDownViewController

        popController.modalPresentationStyle = .popover
        popController.delegate = self
        popController.popoverPresentationController?.permittedArrowDirections = .up
        popController.popoverPresentationController?.delegate = self
        popController.popoverPresentationController?.sourceView = filterButton
        popController.popoverPresentationController?.sourceRect = filterButton.bounds

        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        let context = appDelegate.persistentContainer.viewContext

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
        var predicate = NSPredicate(format: "code matches[c] '\(code!)'")
        fetchRequest.returnsObjectsAsFaults = false
        fetchRequest.predicate = predicate
        let entity = NSEntityDescription.entity(forEntityName: "Item",
                                                in: context)

        fetchRequest.resultType = .dictionaryResultType
        let entityProperties = entity?.propertiesByName
        let filterToFetch = "filter\(filterButton.tag)"
        let propertiesToFetch: [Any] = [entityProperties![filterToFetch]!]
        fetchRequest.propertiesToFetch = propertiesToFetch
        fetchRequest.returnsDistinctResults = true

        var result = [[String : String]]()

        do {
            result = try context.fetch(fetchRequest) as! [[String : String]]
        } catch {
            print("Unable to fetch managed objects for Item).")
        }

        var filterArray = [Filter]()
        for dict in result {
            if let search = dict[filterToFetch] {
                predicate = NSPredicate(format: "code matches[c] '\(search)'")
                let filterCode = DatabaseHelper.fetchRecordsForEntity(entity: "Filter",
                                                                      managedObjectContext: context,
                                                                      predicate: predicate) as! [Filter]

                filterArray.append(filterCode.first!)
            }
        }

        popController.filterArray = filterArray

        present(popController, animated: true, completion: nil)
}

Upvotes: 1

Views: 399

Answers (1)

Jon Rose
Jon Rose

Reputation: 8563

You can do it with these simple steps:

  1. Create a NSFetchResultsController with the appropriate predicate based on the current filter settings (use a NSCompoundPredicate), and fetch with it. Set the view controller as the delegate and reload the data of the collectionView when data changes (it seems you don't expect data to be changing while the user is viewing it, so you can just keep it simple). Don't forget to reload the collectionView when updating the NSFetchResultsController.
  2. Now run through all the fetchedObjects in the NSFetchResultsController and see what filterable properties it has. Add those properties to a set (one for each filter category). Then look at the set to determine what filters to display and update the UI.
  3. When the filter changes set the delegate of the current NSFetchResultsController to nil, before creating a new one and the create a new one as described in step one.

In the code you shared you are needlessly doing an complicated fetch to figure out which filters are relevant. I don't know if your code is correct or not, but I do know that it is complicated. And it is faster to just look at the properties in the managedObject that you already have access to in the fetchResultsController. Those items are already fetched and in memory - so it doesn't need to hit the database again. And filtering those item in code is easier than figuring out how to write a complex predicate.

Upvotes: 1

Related Questions