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
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.
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.