Apply sortDescriptor to NSFetchRequest after grouping - swift

I am building a Showroom that has 45000+ catalogs each represented as Literature in CoreData.
To optimise search - instead of searching over Literature, I've added another entity - LiteratureSearchIndex in CoreData, that has searchvalue (either title word/keyword/tag etc. that represents part of the Literature), score (to weight the results) and literatureid.
When the user types something in the search or filters - I do a fetchRequest on the LiteratureSearchIndex, group the results and sort them on the total score.
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "LiteratureSearchIndex")
let entityDescription = NSEntityDescription.entity(forEntityName: "LiteratureSearchIndex", in: moc)
let sumDescription = NSExpressionDescription()
sumDescription.name = "totalScore"
sumDescription.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "score")])
sumDescription.expressionResultType = .integer64AttributeType
if let literatureid = entityDescription?.attributesByName["literatureid"] {
request.propertiesToGroupBy = [literatureid]
request.propertiesToFetch = [literatureid, sumDescription]
request.havingPredicate = NSPredicate(format: "%# >= %#", NSExpression(forVariable: "totalScore"), NSNumber(value: minScore))
}
request.predicate = ...
request.resultType = .dictionaryResultType
request.fetchBatchSize = 50
I'm currently sorting the results after the moc.fetch() like this:
results.sort(by: { (a: [String: Any], b: [String: Any]) -> Bool in
if let aTotalScore = a["totalScore"] as? Int, let bTotalScore = b["totalScore"] as? Int {
return aTotalScore > bTotalScore
} else {
return true
}
})
Is there a better/faster way to sort the results (similar to applying havingPredicate over the array of dictionaries)?

Related

how to modify my Core Data attributes of an entity but on all the elements of my database

hello community I am a novice and this is my first question.
how to change all the attributes of an entity and be able to change all my Core Data elements,
because I can only change the first attribute of an entity but not all my data records.
Here in this function I can only change the name
and then I get this following error has the line:
let objectUpdate = test[0] : Thread 1: Fatal error: Index out of range
func updateData() {
var newName = ""
var newPrenom = ""
newName = name.text!
newPrenom = prenom.text!
let managedContext = AppDelegate.viewContext
let fetchRequest : NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "name = %#", newName)
do {
fetchRequest.predicate = NSPredicate(format: "prenom = %#", newPrenom)
let test = try! managedContext.fetch(fetchRequest) as! [NSManagedObject]
let objectUpdate = test[0]
objectUpdate.setValue(newName,forKey: "name")
objectUpdate.setValue(newPrenom, forKey: "prenom")
do {
try managedContext.save()
}
catch {
print(error)
}
} catch {
print(error)
}
}
There are a number of ways we can avoid this error.
Unwrapping optional .first value
Swift's Collection gives us safe way to get first item, simply by accessing the first property on a given collection. It will return an Optional<Element> value so we need to unwrap it first either by using if let of guard let
if let object = test.first {
// do something with object
}
or
guard let object = test.first else { return }
// do something with object
Checking if value at index exists
It's often a good idea to check for a specific index within the indices property before accessing the value behind it.
if test.indices.contains(0) {
let object = test[0]
// do something with object
}
These hints should prevent your code from crashing again.
Other Suggestions
This is not really safe or clean:
var newName = ""
var newPrenom = ""
newName = name.text!
newPrenom = prenom.text!
We can make it much cleaner and most importantly safer by using a guard statement
guard let newName = name.text, let newPrenom = prenom.text else { return }
Two important things happened here:
No more force-unwrapping the optional values of text [which could cause a crash]
The properties are now immutable, meaning we can be sure that what we are saving to the CoreDate is what was retreived at the beginning of the function
Since the line:
let test = try! managedContext.fetch(fetchRequest) as! [NSManagedObject]
is already wrapped in the do-catch clause, you can safely remove forced try! and replace it with try.
let test = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
Let's use types! On this line you create a NSFetchRequest object for some entity named "Person".
let fetchRequest : NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "Person")
I am guessing CoreData have generated for you a NSManagedObject subclass, named Person. If this is true, you could rewrite it like this:
let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
With the previous tip implemented, we can now get rid of as! [NSManagedObject] from this line:
let test = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
Since the NSFetchRequest object is now nicely typed, we can take advantage of it by rewriting it like this:
let test: [Person] = try managedContext.fetch(fetchRequest)
So we are using proper types now? cool! Lets now improve this:
objectUpdate.setValue(newName,forKey: "name")
objectUpdate.setValue(newPrenom, forKey: "prenom")
by rewriting this and using properties on Person object
objectUpdate.name = newName
objectUpdate.prenom = newPrenom
No need for introducing second level of do-catch clause, since we are already in one!
do {
try managedContext.save()
}
catch {
print(error)
}
you can easily replace it with just the save() call, like this:
try managedContext.save()
Are you sure these predicates are what you want?
fetchRequest.predicate = NSPredicate(format: "name = %#", newName)
fetchRequest.predicate = NSPredicate(format: "prenom = %#", newPrenom)
What I can read from them is that you are fetching Person object where the name is newName and prenom is newPrenom and then you update it with the same exact values? Are you using some kind of identification of users? like id: Int or id: UUID? It would make much more sense to write something like this
let id: Int = // ID of the user you are currently editing
fetchRequest.predicate = NSPredicate(format: "id == \(id)")
if you are not using any id's, you could try storing the initial values of name and prenom
// in cell declaration - set when you configure your cell
var initialName: String?
var initialPrenom: String?
// then in your function:
fetchRequest.predicate = NSPredicate(format: "name = %#", initialName)
fetchRequest.predicate = NSPredicate(format: "prenom = %#", initialPrenom)
But I just noticed you also override you first predicate with the second one. You need to use NSCompoundPredicate
fetchRequest.predicate = NSCompoundPredicate(
type: .and, subpredicates: [
NSPredicate(format: "name = %#", initialName),
NSPredicate(format: "prenom = %#", initialPrenom)
]
)
Suggested version
func updateData() {
guard let newName = name.text, let newPrenom = prenom.text else { return }
let managedContext = AppDelegate.viewContext
let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
fetchRequest.predicate = NSCompoundPredicate(
type: .and, subpredicates: [
NSPredicate(format: "name = %#", initialName),
NSPredicate(format: "prenom = %#", initialPrenom)
]
)
do {
let objects: [Person] = try managedContext.fetch(fetchRequest)
guard let object = objects.first else { return }
object.name = newName
object.prenom = newPrenom
try managedContext.save()
} catch {
print(error)
}
}
If the index 0 is out of range, it means that the array is empty. Before accessing it, add
if test.isEmpty{
return //the fetch request didn't return any values
}

core data fetch with filter

I'm trying to add a filter (boolean value) to a fetch in core data, but I'm not sure how.
The entity name is Vinyl and the attribute that I want to use to filter is called wishlist (true or false)
if let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext {
if let vinyls = try? context.fetch(Vinyl.fetchRequest()) {
if let theVinyls = vinyls as? [Vinyl] {
self.allVinyls = theVinyls
totalVinyls = theVinyls.count
}
}
}
How can I filter for wishlist == true ?
Add an appropriate predicate.
All if lets are not necessary, the viewContext is supposed to be non-optional.
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request : NSFetchRequest<Vinyl> = Vinyl.fetchRequest()
let predicate = NSPredicate(format: "wishlist == TRUE")
request.predicate = predicate
do {
self.allVinyls = try context.fetch(request)
totalVinyls = self.allVinyls.count
} catch { print(error) }

How to append the contents of NSSet to [NSManagedObject]?

My Code
isFiltering = true
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let words = textInSearchField.components(separatedBy: " ")
for word in words{
if (word).count == 0{
continue
}
let firstNamePredicate = NSPredicate(format: "firstName contains[c] %#", word)
let lastNamePredicate = NSPredicate(format: "lastName contains[c] %#", word)
let idPredicate = NSPredicate(format: "id contains[c] %#", word)
let orPredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.or, subpredicates: [firstNamePredicate, lastNamePredicate, idPredicate])
clientsEntity.predicate = orPredicate
clientResults = try! context.fetch(clientsEntity) as! [NSManagedObject]
let sort:NSSortDescriptor = NSSortDescriptor(key:"dateSorted", ascending: false)
for (index, ob) in clientResults.enumerated(){
let relationship = ob.value(forKey: "assessed_by") as! NSSet
let array = relationship.sortedArray(using: [sort]) as! [NSManagedObject]
for item in array.enumerated() {
results.append(item.element)
print(results)
}
}
My data model:
I am using a tableView to display my data which works great, now I have implemented a filter function which allows the user to search based on a Clients first name, last name, id etc using NSCompoundPredicate.
I then sort the resulting [NSManagedObject] by date using NSSortDescriptor, my aim is to set my clientResults variable to contain the SORTED contents of the NSSet. My print statement only outputs that there is one Assessment inside the results variable when in actual fact the NSSet contains two of these NSManagedObjects.
let sort:NSSortDescriptor = NSSortDescriptor(key:"dateSorted", ascending: false)
for (index, ob) in clientResults.enumerated(){
let relationship = ob.value(forKey: "assessed_by") as! NSSet
let array = relationship.sortedArray(using: [sort]) as! [NSManagedObject]
// MARK - I enumerate the contents of the sorted array.
for item in array.enumerated() {
results.append(item.element)
print(results)
}
}
What is the best practice for assigning the contents of the NSSet to a variable of type [NSManagedObject]?
Thank you.
If you know that elements in NSSet are of type NSManagedObject why not just do
let managedObjectsArray = set.allObjects
or if you want to make sure it is of correct type you can do:
if let managedObjectsArray = set.allObjects as? [NSManagedObject] {
//do what you want with [NSManagedObject] array
}

Predicate in realm with dictionary parameters

I want to create a predicate with a dictionary paramateres and filter data on realm, like
var parameters = [String: Any]()
parameters["Mobile"] = a.Mobile!
parameters["CategoryId"] = self.SelectCategryId
let existContact = (contactBiz.FetchMulti(parameters: parameters)?.count)! > 0
and in fetchMulti func I make a predicate and filter data
func FetchMulti(parameters: [String: Any])-> Results<RealmEntityType>?
{
do
{
let key = T.KeyName()
var object = realm.objects(RealmEntityType.self)
let subPredicates = parameters.map {
NSPredicate(format: "%# = %#", $0.key, $0.value as! CVarArg)
}
let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: subPredicates)
// var predictionArray = [String]()
// for p in parameters
// {
// predictionArray.append("\(p.key) = \(p.value) ")
//
// }
//
// let perdicate = predictionArray.joined(separator: " && ")
//
return object.filter(compoundPredicate);
}
catch
{
print(error.localizedDescription)
}
return nil
}
but I get this error
reason: 'Predicate expressions must compare a keypath and another keypath or a constant value'
Any help!
You need to use %K to indicate that the value is the name of a key and not a constant string: NSPredicate(format: "%K = %#", $0.key, $0.value as! CVarArg)

Core Data - How can I get the max value from an entity attribute (Swift)

Recipe
recipeID: Int
recipeName: String
I have an entity Recipe with an attribute recipeID.
How can I get the max(recipeID) as an Int value in Swift?
I'm new in swift, please help me.
Thanks in advance.
func fetchMaxID() {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Recipe")
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "recipeID", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let maxID = try [managedObjectContext?.executeFetchRequest(fetchRequest)].first
print(maxID)
} catch _ {
}
}
The way that Apple recommends and is the fastest is using NSExpressions. moc is a NSManagedObjectContext.
private func getLastContactSyncTimestamp() -> Int64? {
let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest()
request.entity = NSEntityDescription.entity(forEntityName: "Contact", in: self.moc)
request.resultType = NSFetchRequestResultType.dictionaryResultType
let keypathExpression = NSExpression(forKeyPath: "timestamp")
let maxExpression = NSExpression(forFunction: "max:", arguments: [keypathExpression])
let key = "maxTimestamp"
let expressionDescription = NSExpressionDescription()
expressionDescription.name = key
expressionDescription.expression = maxExpression
expressionDescription.expressionResultType = .integer64AttributeType
request.propertiesToFetch = [expressionDescription]
var maxTimestamp: Int64? = nil
do {
if let result = try self.moc.fetch(request) as? [[String: Int64]], let dict = result.first {
maxTimestamp = dict[key]
}
} catch {
assertionFailure("Failed to fetch max timestamp with error = \(error)")
return nil
}
return maxTimestamp
}
Learning from Ray Wenderlich's Core Data Tutorial
https://www.raywenderlich.com/115695/getting-started-with-core-data-tutorial/
func fetchMaxRecipe() {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Recipe")
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "recipeID", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let recipes = try context.executeFetchRequest(fetchRequest) as! [Recipe]
let max = recipes.first
print(max?.valueForKey("recipeID") as! Int)
} catch _ {
}
}
Hope this helps =).
func fetchMaxID() {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Recipe")
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "recipeID", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let results = try context.executeFetchRequest(fetchRequest) as! [Recipe]
if (results.count > 0) {
for result in results {
print(result.recipeID!)
}
} else {
print("No Recipe")
}
} catch let error as NSError {
// failure
print("Fetch failed: \(error.localizedDescription)")
}
}
This works also!
To check the max of an entity attribute you can simply use:
func findMaxRecipeID() -> Int {
let maxRecipeID = recipe?.value(forKeyPath: "recipe.#max.recipeID") as! Int
return maxRecipeID
}
You can use this to #sum, #max, #min, #count - saves quite a few lines of code.