How to set NSFetchRequest propertiesToFetch with a custom function using NSExpression - ios

I have a attribute "message" of type String in CoreData Entity, which stores text and sometimes including URLs . Now i need to fetch all the url's from the entity.
let predicate = NSPredicate(format:"message CONTAINS[cd] %#", "http")
let fetchRequest = self.getfetchRequest(entityName: "Messages", sectionKey:nil, predicate:predicate)
let expressionDescription = NSExpressionDescription()
expressionDescription.name = "urlFromMsg"
expressionDescription.expressionResultType = .StringAttributeType
let expression = NSExpression(forFunction: "toUrl", arguments: [NSExpression(forKeyPath: "message")])
expressionDescription.expression = expression
fetchRequest.propertiesToFetch = [expressionDescription]
fetchRequest.resultType = NSFetchRequestResultType.DictionaryResultType
Here toUrl is a custom function that extends String.
extension String {
func toUrl()->[String]?{
let detector = try! NSDataDetector(types: NSTextCheckingType.Link.rawValue)
let matches = detector.matchesInString(self, options: [], range: NSRange(location: 0, length: self.utf16.count))
var urls = [String]()
for match in matches {
urls.append((self as NSString).substringWithRange(match.range))
}
return urls
}
}
i am getting crash at this line
let expression = NSExpression(forFunction: "toUrl", arguments: [NSExpression(forKeyPath: "message")])
How to set the custom method properly in NSExpression.

When using a custom NSExpression in a fetch request and a SQLite-backed Core Data store, Core Data attempts to translate the expression into a SQL query. That helps with performance, but it means that not all valid NSExpressions are valid for use with Core Data.
Specifically, an NSExpression that relies on a custom function can't be used with a Core Data fetch, because Core Data can't translate arbitrary code into SQL.
You'll need to drop this NSExpression from the fetch and convert to URLs using the results of the fetch. You could also switch to a non-SQLite Core Data store, but that's generally much worse for a variety of reasons unless your persistent store contains very small amounts of data.

Related

Is it possible to perform NSBatchUpdateRequest with an array of NSManagedObjectID?

Currently, I perform multiple update operations via the following code.
func updateOrders(_ updates : [(objectID: NSManagedObjectID, order: Int64)]) {
if updates.isEmpty {
return
}
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
backgroundContext.perform {
for update in updates {
let objectID = update.objectID
let order = update.order
let nsPlainNote = try! backgroundContext.existingObject(with: objectID) as! NSPlainNote
nsPlainNote.order = order
}
RepositoryUtils.saveContextIfPossible(backgroundContext)
}
}
Since I would like to
Make the update operations run faster
Avoid delegate of NSFetchedResultController from being notified
I would like to utilise NSBatchUpdateRequest for performing such update operation.
However, I don't find a way, how I can apply array of NSManagedObjectID and array of Int64 value, to NSBatchUpdateRequest.
Given an array of NSManagedObjectID and Int64, is it possible to use NSBatchUpdateRequest to perform updated on CoreData?
You must use NSPredicate to set object id
func updateOrders(_ updates : [(objectID: NSManagedObjectID, order: Int64)]) {
updates.forEach {
let request = NSBatchUpdateRequest(entityName: "NSPlainNote")
request.propertiesToUpdate = ["order": $0.order]
request.predicate = NSPredicate(format: "objectID == %#", $0.objectID)
let result = try? context.execute(request)
}
}
NSBatchUpdateRequest is not suitable for your task since using it makes sense for large amount of records with a common attribute's value so that you can filter all by your criteria and update all fields with your values at once.
The fact is that the NSBatchDeleteRequest is an NSPersistentStoreRequest which operates at the SQL level in the persistent store itself and it doesn't update your in-memory objects after execution thats why it works so fast and Core Data translates your native requests to a SQL ones where you can not use dynamically code to get and insert needed data from dictionary etc. but you can update the current value of a filed e.g.:
let batchRequest = NSBatchUpdateRequest(entityName: "Note")
batchRequest.predicate = predicate
// Increase `order` value
batchRequest.propertiesToUpdate = ["order" : NSExpression(format: "order + 1")]
do {
try context.execute(batchRequest)
}
catch {
fatalError(error.localizedDescription)
}

Do this to core data: "Select * WHERE "attribute" == "Some string" "

I did manage to do this by making a function with a loop that checks the attribute to a string. But i'm looking for a better way to do this.
In sql I do this:
Select * WHERE "attribute" == "string"
Is there a method to do this in swift?
My function looks like this:
func tableData()
{
let objects = retrieveValues("JobTime") //Retrieve a NSMutableArray
if !objects.isEmpty
{
for var index = 0; index < objects.count; ++index
{
if objects[index].valueForKey("jobTitle") as? String == transferTitle
{
print("Job title matched: \(index)")
}
else
{
print("Nothing here!")
}
}
}
}
In order to perform fetch request in CoreData you have to initialise NSFetchRequest class. In order to specify in what kind of entities you are interested you create NSPredicate class. It gives you ability to specify pretty advanced queries. In most cases the simplest way to create NSPredicate is by using format string - details about the syntax can be found Apple's Predicate Format String Syntax document.
You can find example of how you can perform fetch request in CoreData (and Swift) below.
let managedObjectContext: NSManagedObjectContext
let predicate = NSPredicate(format: "jobTitle == %#", "Programmer")
let fetchRequest = NSFetchRequest.init(entityName: "People")
fetchRequest.predicate = predicate
//fetchRequest.sortDescriptors = [] //optionally you can specify the order in which entities should ordered after fetch finishes
let results = managedObjectContext.executeFetchRequest(fetchRequest)
You can pass the query to CoreData and only retrieve what you want. The NSManagedObjectContext class has a executeFetchRequest method that you call to retrieve data from the database. You pass NSFetchRequest object to it. That object contains a NSPredicate, which defines your query.

Using regex in NSPredicate to perform a NSFetchRequest

I try to perform an NSFetchRequest with this NSPredicate:
let searchString: NSString = "приш[её]л"
let predicate = NSPredicate(format: "text MATCHES[cd] %#", searchString)
let fetchRequest = NSFetchRequest(entityName: "Data")
fetchRequest.predicate = predicate
do {
let objects = try context!.executeFetchRequest(fetchRequest) as! [NSManagedObject]
return objects
} catch {
print("Error")
}
But no results, though actually 'text' contains "пришёл"
I've found answer in this qustion: Setting up a CoreData regular expression predicate for a comma-separated list
let searchString = "приш[её]л"
let format = "(text MATCHES %#)"
let pattern = NSString(format: ".*(\\b%#\\b).*", searchString)
let predicate = NSPredicate(format: format, pattern)
You need to be aware that using MATCHES[cd] to perform regex (regular expression) matching is very expensive if you have a lot of data in your database.
You should try to avoid regular expressions if you can. And in this particular case it seems like using normalized text would allow you to use a predicate of the form %K BEGINWITH[n] %# or %K ==[n] %# wich is very fast.
To use normalized strings, you’d have both an attribute text and a normalized version of it, e.g. textNormalized which you could update in the setter for text. You’d then use textNormalized in the [n] predicates.
You can normalize text with
extension String {
public var normalizedForSearch: String {
let transformed = applyingTransform(
StringTransform("Any-Latin; Latin-ASCII; Lower"),
reverse: false)
return transformed as String? ?? ""
}
}
Be sure to check the chapter on Text in our book: https://www.objc.io/books/core-data/ — it describes how to use normalized versions of your text to make sure searches like these are efficient.

How to cast to generic type in Swift

I am about to learn Swift. I want to write a class, that handles Cora Data tasks for me. In this case, it should be possible to pass an entity name and and array of tuples to a function, which then interprets the tuples as key value pairs for NSPredicates to retrieve ManagedObjects matching them. Because of NSPredicate needing values with specific types, I tried to pass the types of the values, too. But I can't get around that "'t' is not a type" compile error (last line of code). How can I cast the value to an passed type? Or would you create the NSPredicate in a totally different way?
class func getManagedObjectsWithEntityName<T>(entityName: String, keysAndValues: [(key: String, value: AnyObject, type: T.Type)]) -> [NSManagedObject] {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: entityName)
var predicates: [NSPredicate] = []
for tuple in keysAndValues {
let t = tuple.type
predicates.append( NSPredicate(format: tuple.key + " = %#", tuple.value as t)!)
}
...
}
No need to use generics here, you don't seem to need to have to access any of the members / functions of whatever class you would use as a generic.
What you do have however is a choice: either you can take values as conforming to CVarArgType, allowing you to plug them straight into your NSPredicate constructor, or you can take values as strings and construct the predicate by building a string expression. Here is an example using CVarArgType:
func getManagedObjectsWithEntityName(entityName: String, keysAndValues:
[(key: String, value: CVarArgType)]) -> [NSManagedObject] {
...
let fetchRequest = NSFetchRequest(entityName: entityName)
var predicates: [NSPredicate] = []
for tuple in keysAndValues {
predicates.append( NSPredicate(format: "%K = %#", tuple.key, tuple.value))
}
...
}
The reason your generic wasn't working is that you set up a place holder , but then your tuple.type in the function sig was actually T.Type (rather than just T). If you changed the tuple specification to (key: String, value: AnyObject, type: T) then you would be able to cast let t = tuple.type as! T. But then you would run into problems as NSPredicate doesn't take type T - whence the solution not really being to use generics, but instead use something like the above. It's also worth bearing in mind it's a good idea to avoid using variable names such as .type and .value, as in certain cases these will cause confusion with things, especially with NSManagedObjects (which have .value). At least it's worth knowing that this can be a cause of issues...

NSPredicate to use with Transformable property

I have array of strings which I save in Core Data as Transformable. From json I am gettings needed strings like this:
if let mealTimes = dictionary["mealTimes"] as? [String]{
self.mealTimes = mealTimes
}
Now I would like to filter fetch results by strings in mealTime property. I have tried this way:
let predicate = NSPredicate(format: "mealTimes LIKE[cd] '*%#*'", "breakfast")
var breakfastContents = StaticContent.fetchStaticContentsWithPredicate(predicate, inManagedObjectContext: self.coreDataStack.context)
if (breakfastContents.count > 0) {
breakfast = breakfastContents.first!
}
The problem is that result array is empty but I know I have breakfast string in some content. So how can I fix it? I was reading something about saving transformable as NSData so it would need some great trick. I was trying to use LIKE (with and without *) command and CONTAINS.
Extension for StaticContent:
class func fetchStaticContentsWithPredicate(predicate: NSPredicate, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> [StaticContent] {
// Define fetch request/predicate
var fetchRequest = NSFetchRequest(entityName: "StaticContent")
// Assign fetch request properties
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
// Handle results
let fetchedResults = managedObjectContext.executeFetchRequest(fetchRequest, error: nil) as! [StaticContent]
return fetchedResults
}
You should refactor your data model to get rid of the "array of strings". This is not a very sound way to store this type of values.
Instead, consider a relationship with a new entity that captures those strings. Alternatively, device a scheme where you store a string and have a unique character that separates the records (e.g ; or something similar). Then any simple string predicate would work, like this:
NSPredicate(format: "mealtimes contains[cd] %#", "breakfast")

Resources