Dynamic Sort Descriptors and Predicates
I based most of my core data code for Retrospective Timelines on an example project that you can check out here. The developer sent me the link to this in a comment on stack overflow a couple of months ago. Throughout the project I’ve made small changes to this to better suite my needs.
Today I made a huge set of changes. I wanted a way to build UI controls that can modify the sort descriptors and predicates that the fetch requests used to drive the FRC. In the sample project this is done by passing in some optional strings, then parsing them into the objects they need to be in the a function that prepares the fetch request. This approach did not scale for what I need, as some of the layouts need complex sorting and/or predicates. I made a new version of this that replaces the optional string properties with some alternatives.
First the sort descriptors. Core Data has a way to apply multiple sort descriptors by passing them as an array. Even if I only have one sort order (rare for this project) I can just pass a single descriptor in the array
private var sortDescriptors: [NSSortDescriptor]?
The predicates were a bit different. Core data accepts one predicate for a fetch request, not multiple… kinda. Predicates can be combined using compounds. I can make compound predicates on each layout as needed.
private var predicate: NSPredicate?
Then I needed a way to add these to the fetched request.
private func configureFetchRequest() -> NSFetchRequest<T> {
let fetchRequest: NSFetchRequest<T> = T.fetchRequest() as! NSFetchRequest<T>
fetchRequest.fetchBatchSize = 0
if let sortDescriptors = self.sortDescriptors {
fetchRequest.sortDescriptors = sortDescriptors
}
if let predicate = self.predicate {
fetchRequest.predicate = predicate
}
return fetchRequest
}
I need a way to call this publicly as well. This calls the private method after checking the optionals. If I no longer have a descriptor or predicate, I set the property back to nil so it’s no longer used in configureFetchRequest
public func loadDataSource(sort: [NSSortDescriptor]?, predicate: NSPredicate?) -> [T] {
if let sort = sort {
self.sortDescriptors = sort
} else {
self.sortDescriptors = nil
}
if let predicate = predicate {
self.predicate = predicate
} else {
self.predicate = nil
}
self.fetchRequest = configureFetchRequest()
self.frc = configureFetchedResultsController()
return self.allInOrder
}
Now for the cool part. I can make a View Model for each layout where I can place some properties to drive controls in the user interface. This View Model will also handle building the sort descriptors and predicates for the layout. They can then be used as a parameter when I call loadDataSource
.
Here is a basic example of the View Model that drives the list of event dates. The sortToggle
variable is bound to a UI toggle so when the user taps it the sort order changes. I’ll replace this with a better sort button, but the underlying concept will remain the same.
class EventListVM: ObservableObject {
@Published var sortToggle = false
public func getSort() -> [NSSortDescriptor] {
return [NSSortDescriptor(key: "isOngoing", ascending: sortToggle), NSSortDescriptor(key: "date", ascending: sortToggle)]
}
public func getPredicate(timeline: Timeline?) -> NSPredicate? {
if let timeline = timeline {
let startString = String(format: "%@%@", "dateStartEvent.eventTimeline", " == %@")
let startPredicate = NSPredicate(format: startString, timeline)
let endString = String(format: "%@%@", "dateEndEvent.eventTimeline", " == %@")
let endPredicate = NSPredicate(format: endString, timeline)
let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [startPredicate, endPredicate])
return compoundPredicate
}
return nil
}
}
The only thing that is a little nuts is the way I get the records to use in the ForEach
view. This calls a method on one ObservedObject while using return values from two functions on another ObservedObject. I feel like I’m getting away with something here.
@ObservedObject var dataSource = RADDataSource<RTDate>()
@ObservedObject var eventListVM = EventListVM()
...
ForEach(dataSource.loadDataSource(sort: self.eventListVM.getSort(), predicate: self.eventListVM.getPredicate(timeline: self.timeline))) { rtDate in
...
}
This is just a simple example, but now that I have the foundation in place I can extend this to work on much more complex user interfaces.