简体   繁体   中英

Filtering a SectionedFetchRequest in SwiftUI on iOS15

I am trying to implement the new iOS 15 SectionedFetchRequest with Core Data. This allows you to change the predicates and sort descriptors at runtime.

My model has a Group , which can contain many Item s.

I declare the fetch request like this, then assign to it in the view's init method, so that I can apply a custom NSPredicate depending on where the view is being shown.

@SectionedFetchRequest private var mySections: SectionedFetchResults<String, Item>

The they are displayed in a ForEach loop, similar to the code from the WWDC 2021 session .

var body: some View {
    // Other code
    List {
        ForEach(mySections) { section in
            Section(header: Text(section.id) {
                ForEach(section) { item in
                    Text(item.name) // Just an example
                }
            }
        }
    }
}

I am also implementing the .searchable view modifier, to provide search functionality. This is bound to a string variable like this.

@State private var searchText: String = ""

Then in the body property I use .searchable($searchText) on the bottom of the list.


This all works without issues. However, the problem comes when I try to use the search text to filter my results at runtime. I tried using a dynamic predicate instead and changing the fetch request predicate (compounded with the one from initialisation), but this proved very buggy (maintaining the search state when the view popped to a detail view was very difficult and led to details views popping unexpectedly).

So, I decided to filter the fetch request in runtime with standard Swift code. I have done this before easily with a one-dimensional array. However, SectionedFetchRequest is not just a two-dimensional array; it is a generic struct with Section and Element types and the section has an important id property (used to define the section heading).

Standard two dimensional filtering (such as this answer does not work, as the types are lost and you merely get a two-dimensional array.

I can easily get the sections that have an item that fits the search, but this returns all of the items in the section (rather than just the ones that fit the search):

mySections.filter { section in
    section.contains(where: checkItemMethod())
}

I tried to then make my own type that conformed to RandomAccessCollection (required by the ForEach ) that I could populate recursively, but this seems very complex as there are so many "sub-protocols" to conform to.

Is there a way to easily filter a SectionedFetchRequest based on the inner objects, and only returning sections containing objects that match?

Thanks


EDIT

Here is my view init code. The SelectedSort type is straight out of the WWDC video. I just persist the by and order values in User Defaults.

init(filterGroup: DrugViewFilterGroup) {
        
    self.filterGroup = filterGroup
    
    // This relates to retrieving persisted value of the SelectedSort type demonstrated in WWDC
    let sortBy = UserDefaults.standard.integer(forKey: "sortBy")
    let sortOrder = UserDefaults.standard.integer(forKey: "sortOrder")
    let selectedSort = SelectedSort(by: sortBy, order: sortOrder)
    let sectionIdentifier = sorts[selectedSort.index].section
    let sortDescriptors = sorts[selectedSort.index].descriptors
    
    // Create sectioned fetch request
    let sfr = SectionedFetchRequest<String, Item>(sectionIdentifier: sectionIdentifier, sortDescriptors: sortDescriptors, predicate: filterGroupPredicate(for: filterGroup), animation: .spring())
    
    // Assign fetch request to the @SectionedFetchRequest property of the view
    self._mySections = sfr
}

This all works just fine. The filterGroupPredicate(for:) method just returns an NSPredicate to show a subset of data.

The issue is with implementing the search bar with the .searchable modifier. This is bound to @State private var searchText: String .

Here is an example function filtering the results by the search text:

private func getFilteredSections() -> [SectionedFetchResults<String, Item>.Section] {
    let cleanedFilter = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
    guard !cleanedFilter.isEmpty else { return mySections.map { $0 } }
    
    func checkItem(_ item: Item) -> Bool {
            return item.name.localizedCaseInsensitiveContains(cleanedFilter) 
    }
        
    return mySections.filter { section in
        section.contains(where: checkItem)
}

This works, but the problem is it only returns the "top level" ie the sections themselves. If a section contains a single item meeting the search criteria, all items in the section will still be returned.

Filtering it like a multidimensional array (as noted in original question), results in a simple 2D array with no section information to display in the nested ForEach , and thus the functionality of the SectionedFetchRequest is lost.

I tried setting the fetch request predicate (to a compound predicate based on the one set in init and one for the search text). This works briefly, but as the view is reinitialised on eg navigating to a tapped detail view, it results in erratic search bar behaviour.

SectionedFetchResults has a property nsPredicate . If you set that with your desired NSPredicate(format:) , the results are updated more or less immediately.

Works with SwiftUI search with something like:

mySection.nsPredicate = NSPredicate(format: "name contains[cd] %@", searchText)

Most of the other code can be erased. You need a little magic to manage displaying the filtered collection.

SectionedFetchResults > nsPredicate in Apple Dev Docs

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM