SwiftUI improvements: SectionedFetchRequest and dynamic changes
As of iOS 15 & MacOS 12, there are some new tools we can work with in SwiftUI:
SectionedFetchRequest and SectionedFetchResults which are like FetchRequest and FetchedResults just with added section support.
And the ability to dynamically update a FetchRequest's (or SectionedFetchRequest's) predicates through the nsPredicate property.
It would be nice to add PredicateKit support for these. I'd be happy to make a PR after I have some time to work on this. Shouldn't be too difficult, just a little time-consuming!
Awesome. Looking forward to reviewing the PR 👍.
Almost done! So far it works to just copy and make small modifications to your existing code, but a test case is throwing an unexpected exception.
Here is a SwiftUI.SectionedFetchRequest extension:
@available(iOS 15.0, watchOS 8.0, tvOS 15.0, macOS 12.0, *)
extension SectionedFetchRequest where Result: NSManagedObject {
public init(fetchRequest: FetchRequest<Result>, sectionIdentifier: KeyPath<Result, SectionIdentifier>, animation: Animation? = nil) {
let entityName = Result.entity().name ?? String(describing: Result.self)
let fetchRequestBuilder = NSFetchRequestBuilder(entityName: entityName)
self.init(fetchRequest: fetchRequestBuilder.makeRequest(from: fetchRequest), sectionIdentifier: sectionIdentifier, animation: animation)
}
}
And here is a test case:
func testSectionedFetchRequestPropertyWrapperWithNoPredicate() throws {
struct ContentView: View {
@SwiftUI.SectionedFetchRequest(
fetchRequest: FetchRequest()
.sorted(by: \.billingInfo.accountType, .ascending)
.sorted(by: \.name, .ascending),
sectionIdentifier: \.billingInfo.accountType
)
var users: SectionedFetchResults<String, User>
var body: some View {
List(users, id: \.id) { section in
Section(section.id) {
ForEach(section, id: \.objectID) { user in
Text(user.name)
}
}
}
}
}
let view = ContentView().environment(\.managedObjectContext, .default)
let request = try XCTUnwrap(
Mirror(reflecting: view).descendant("content", "_users") as? SwiftUI.SectionedFetchRequest<String, User>
)
XCTAssertEqual(request.projectedValue.wrappedValue.nsPredicate, NSPredicate(value: true))
}
Both of those are almost exactly copied from your code from SwiftUI.FetchRequest, but the final XCTAssertEqual throws an exception when calling request.projectedValue.
The exception code is Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
and a warning in the console says Accessing StateObject's object without being installed on a View. This will create a new instance each time.
There must be something different in the underlying property wrapper of SectionedFetchRequest vs FetchRequest. Do you have any ideas how we'd get around that?
There must be something different in the underlying property wrapper of SectionedFetchRequest vs FetchRequest.
Indeed, SectionedFetchRequest has a property _controller of type StateObject<FetchConroller<String, User, SectionedFetchResult<String, User>> (in your example). Trying to read the value of the request must be accessing this property under the hood. This property should be accessed only after the view has been added to the view hierarchy. Otherwise we get the crash you're seeing.
Do you have any ideas how we'd get around that?
Off the top of my head, no. But feel free to open a PR without this test. I'll look into it and figure out something before merging. Might take some time though.
Thanks! I'll open up the PR now.
Changes merged in 31dc2a5182dd4a7ac7375913fde7b28b07a28dee. See https://github.com/ftchirou/PredicateKit/pull/25#issuecomment-2038404030.