Realm Swift background search - swift

I have a realm database in my app, containing a list of ~2000 users.
A tableview displays these users, and a search bar allows to filter them (on 6 different properties of each user).
This operation was blocking the UI, so I put it in a background thread.
Now it's a lot better, but I'm not 100% sure that it's the best way to do this.
Can you suggest any other solutions, if you have any better ?
Here's the sample code I use :
func filterUsers(searchText:String,completion: (result: Array<User>) -> ()){
var IIDS = Array<String>()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
let predicate1 = NSPredicate(format: "firstName contains[c] %#", searchText)
let predicate2 = NSPredicate(format: "lastName contains[c] %#", searchText)
let bgRealm = try! Realm()
bgRealm.beginWrite()
//Filter the whole list of users
var results = Array(bgRealm.objects(User)).filter {
//One user object by one
let usr:User = $0
//Reset the value by default
usr.searchResultField = ""
if predicate1.evaluateWithObject(usr) {
usr.searchResultField = "firstNameORlastName"
return true
}
else if predicate2.evaluateWithObject(usr) {
usr.searchResultField = "IID"
}
return false
};
try! bgRealm.commitWrite()
for usr in results {
IIDS.append("'\(usr.IID)'")
}
results.removeAll()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let realm = try! Realm()
let foundUsers = Array(realm.objects(User).filter("IID IN {\(IIDS.joinWithSeparator(","))}"))
IIDS.removeAll()
completion(result: foundUsers)
})
})
}

You filter the objects after pulling them all into memory (by converting the Results to an Array). You'll have a vastly better performance, if you let Realm handle the filtering. For that purpose you'd need to be able to make all your queries by predicates which you can combine to one OR-compound predicate.
Furthermore I'd avoid storing the matching field in the object to separate concerns as the values are transient. They are only needed as long those objects are kept in memory.
Beside that I'd recommend to use a primary key for IID and then just retrieve one object by another instead of building a huge predicate with all IDs.
To put it all together, this would be the way I'd tackle that:
func filterUsers(searchText:String, completion: (result: Array<(user: User, field: String)>) -> ()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
var predicates = [
"firstName": NSPredicate(format: "firstName contains[c] %#", searchText)
"lastName": NSPredicate(format: "lastName contains[c] %#", searchText)
]
let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: Array(predicates.values))
let bgRealm = try! Realm()
// Filter the whole list of users
let results = bgRealm.objects(User).filter(compoundPredicate)
// Find out which field is matching
let idsAndFields: [(IID: String, field: String)] = results.flatMap {
for (field, predicate) in predicates {
if predicate.evaluateWithObject($0) {
return (IID: $0.IID, field: field)
}
}
return nil
}
dispatch_async(dispatch_get_main_queue()) {
let realm = try! Realm()
let result = idsAndFields.flatMap {
if let user = realm.objectForPrimaryKey($0.IID) {
return (user: user, field: $0.field)
} else {
return nil
}
}
completion(result: result)
})
}
}

Related

How to check if there is duplicate data in coredata's entity when adding data

I am learning core data, and I want to add data, before this, i want to check if there is repeated data in Backpack Entity, if there are duplicate values, return Bool to prompt the user.
so write this function to get the number of repeated data, but may be the reason for the purple error is not effective, do not know how to solve...
purple error:Accessing StateObject's object without being installed on a View. This will create a new instance each time.
Help please...
func RecheckingEquip() -> Bool {
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Backpack.creatTime, ascending: false)],
predicate: NSPredicate(format: "equipedImage == %#", equips.imageName),
animation: .default) var backpackItems: FetchedResults<Backpack>
let number = backpackItems.count
if number == 0 {
return false
}
return true
}
code
i think i know it, Let me answer myself this question
func RecheckingEquip(context: NSManagedObjectContext) -> Bool {
let entityForTableName = NSEntityDescription.entity(forEntityName: "EntityName", in: context)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
let predicate = NSPredicate.init(format: "someAttribute == %#", someVariable)
fetchRequest.predicate = predicate
fetchRequest.entity = entityForTableName
do {
let arrData = try context.fetch(fetchRequest)
if arrData.count > 0 {
print("Record already exist")
return true
} else {
return false
}
} catch {
print(error.localizedDescription)
}
return false
}

Sorting a Realm Results object in the background thread

I am trying to sort a Realm Results instance in a background thread. But I am getting 'Realm accessed from incorrect thread.' exception. What am I doing wrong here?.
I'm using this function to filter and update the table with the result as the text in the search bar text field changes.
Thanks in advance.
var previousSearchWork?
func getInvoicesFor(searchedTerm: String, completion: #escaping ([Invoice]) -> Void) {
previousSearchWork?.cancel()
let newSearchWork = DispatchWorkItem {
guard let realm = try? Realm() else { return }
var filteredInvoices = [Invoice]()
if searchedTerm.first!.isLetter { // searching by customer name
let predicate = NSPredicate(format: "name BEGINSWITH[cd] %# || name CONTAINS[cd] %#", searchedTerm, searchedTerm)
let invoices = realm.objects(Invoice.self).filter(predicate)
filteredInvoices = invoices.sorted {
$0.name!.levenshteinDistance(searchedTerm) < $1.name!.levenshteinDistance(searchedTerm)
}
} else { // searching by id
// ...
}
completion(filteredInvoices)
}
previousSearchWork = newSearchWork
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + .milliseconds(30), execute: newSearchWork)
}
As #Jay has mentioned in a reply to the original question:
... that Realm is on a background thread so the objects are on that thread; what happens with [Invoice] upon completion?
Yep, it turns out I've been fetching Realm persisted objects on a background thread and send it to the caller via completion closure and then the caller tries to read them on main thread. That's what triggered the 'Realm accessed from incorrect thread'
First of all, I couldn't find a way to sort the objects without transforming it to an array of realm objects since I needed to use a custom sorting method.
All I did to fix the above function was instead of returning an array of Objects that are fetched inside a background thread, I am returning references to those objects so I can refer to them in main thread
According to my poor research, I've found two ways to pass those objects from background thread to main thread. (I went for the second way cause as to what've read, it's faster for this case.)
let backgroundQueue = DispatchQueue.global()
let mainThread = DispatchQueue.main
// Passing as ThreadSafeReferences to objects
backgroundQueue.async {
let bgRealm = try! Realm()
let myObjects = bgRealm.objects(MyObject.self)
// ......
let myObjectsArray = .....
let references: [ThreadSafeReference<MyObject>] = myObjectsArray.map { ThreadSafeReference(to: $0) }
mainThread.async {
let mainRealm = try! Realm()
let myObjectsArray: [MyObject?] = references.map { mainRealm.resolve($0) }
}
}
// Passing primaryKeys of objects
backgroundQueue.async {
let bgRealm = try! Realm()
let myObjects = bgRealm.objects(MyObject.self)
// ......
let myObjectsArray = .....
// MyObject has a property called 'id' which is the primary key
let keys: [String] = itemsArray.map { $0.id }
mainThread.async {
let mainRealm = try! Realm()
let myObjectsArray: [MyObject?] = keys.map { mainRealm.object(ofType: MyObject.self, forPrimaryKey: $0) }
}
}
After adjusting the function (and completing it for my need):
var previousSearchWork: DispatchWorkItem?
func getInvoicesFor(searchedTerm: String, completion: #escaping ([String]) -> Void) {
previousSearchWork?.cancel()
let newSearchWork = DispatchWorkItem {
autoreleasepool {
var filteredIDs = [String]()
guard let realm = try? Realm() else { return }
let allInvoices = realm.objects(Invoice.self).filter(NSPredicate(format: "dateDeleted == nil"))
if searchedTerm.first!.isLetter {
let predicate = NSPredicate(format: "name BEGINSWITH[cd] %# || name CONTAINS[cd] %#", searchedTerm, searchedTerm)
let invoices = allInvoices.filter(predicate)
filteredIDs = invoices.sorted {
$0.name!.levenshtein(searchedTerm) < $1.name!.levenshtein(searchedTerm)
}.map {$0.id}
} else {
var predicates = [NSPredicate(format: "%# IN ticket.pattern.sequences", searchedTerm)]
if searchedTerm.count > 3 {
let regex = searchedTerm.charactersSorted().reduce("*") {$0 + "\($1)*"}
let predicate = NSPredicate(format: "ticket.pattern.id LIKE %#", regex)
predicates.append(predicate)
}
let invoices = allInvoices.filter(NSCompoundPredicate(orPredicateWithSubpredicates: predicates)).sorted(byKeyPath: "dateCreated", ascending: false)
filteredIDs = Array(invoices.map {$0.id})
}
DispatchQueue.main.async {
completion(filteredIDs)
}
}
}
previousSearchWork = newSearchWork
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + .milliseconds(30), execute: newSearchWork)
}

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
}

Saving & Fetching CloudKit References

I'm having trouble creating with CloudKit References. Data is being saved into CloudKit but its not referencing its parent (list). Don't know what i'm doing wrong, any help would be much appreciated!
Saving Method
var list: CKRecord?
var item: CKRecord?
#objc func save() {
let name = nameTextField.text! as NSString
//Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
if item == nil {
//Create Record
item = CKRecord(recordType: RecordTypeItems)
//Initialization Reference
guard let recordID = list?.recordID else { return }
let listReference = CKRecord.Reference(recordID: recordID, action: .deleteSelf)
item?.setObject(listReference, forKey: "list")
}
item?.setObject(name, forKey: "name")
//Save Record
privateDatabase.save(item!) { (record, error) in
DispatchQueue.main.sync {
self.processResponse(record: record, error: error)
}
}
}
Fetch Method
var list: CKRecord!
var items = [CKRecord]()
private func fetchItems() {
//Fetch Private Database
let privateDatabase = CKContainer.default().privateCloudDatabase
//Initialize Query
guard let recordID = list?.recordID else { return }
let reference = CKRecord.Reference(recordID: recordID, action: .deleteSelf)
let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: "list == %#", [reference]))
//Configure Query
query.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
//Peform Query
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
DispatchQueue.main.sync {
self.processResponseForQuery(records: records, error: error)
}
}
}
Where you are creating your query to retrieve items referencing the list, should the list reference in the predicate format string be inside an array? If you create the item's reference like item?.setObject(listReference, forKey: "list"), CloudKit will infer the list field to be a single CKRecord.Reference, so the query would be:
let query = CKQuery(recordType: RecordTypeItems, predicate: NSPredicate(format: "list == %#", reference))

reload table with async search result

I large amount of data in my app with search functionality. I am using SQLite and Core Data to search and Fetch data.
Here is my search function,
func fetchSearchResultsWith(_ searchText : String?){
DispatchQueue.global(qos: .background).async {
var resArr : [Int64] = []
let stmt = "SELECT rowid FROM htmlText_fts WHERE htmlText MATCH '\(searchText!)*'"
do {
let res = try self.db.run(stmt)
for row in res {
resArr.append(row[0] as! Int64)
}
} catch {
print(error.localizedDescription)
}
let request : NSFetchRequest<Monos> = Monos.fetchRequest()
request.fetchLimit = 200
let predicate = NSPredicate(format: "id in %#", resArr)
request.predicate = predicate
var arr : [Items]? = []
do {
arr = try context.fetch(request)
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async(execute: {
self.monosSearchResult = arr
self.tableView.reloadData()
})
}
}
I am using DispatchQueue.global.async to avoid freezing UI, but then its returning async array and my table view ends up reloading with wrong result. If I use DispatchQueue.global.sync it works fine, but then my UI freezes when I type in to searchBar. I am not sure what I can do get right result. Any help will be appreciated!
Please let me know if you need any further information.
Since you have a 2 step search mechanism , a new search may be initiated before the other ones end , so to lightWeight this operation , store the last value of the textfield inside a var
lastSear = textfield.text
fetchSearchResultsWith(lastSear)
then do this inside the search function in 3 places
Before search the DB & after & before setting the array and reloading the table
if searchText != lastSear { return }
You have not included your table data source methods which populate the table, but I assume you are using values from self.monosSearchResult. If not, then your fetch code is populating the wrong values, and that may be part of your problem.
Additionally, your fetch request needs to be running on the appropriate thread for your NSManagedObjectContext, not necessarily (probably not) the global background queue. NSManagedObjectContext provides the perform and performAndWait methods for you to use their queues properly.
func fetchSearchResultsWith(_ searchText : String?){
// context: NSManagedObjectContext, presumably defined in this scope already
// since you use it below for the fetch.
// CHANGE THIS
// DispatchQueue.global(qos: .background).async {
// TO THIS
context.perform { // run block asynchronously on the context queue
var resArr : [Int64] = []
let stmt = "SELECT rowid FROM htmlText_fts WHERE htmlText MATCH '\(searchText!)*'"
do {
let res = try self.db.run(stmt)
for row in res {
resArr.append(row[0] as! Int64)
}
} catch {
print(error.localizedDescription)
}
let request : NSFetchRequest<Monos> = Monos.fetchRequest()
request.fetchLimit = 200
let predicate = NSPredicate(format: "id in %#", resArr)
request.predicate = predicate
var arr : [Items]? = []
do {
arr = try context.fetch(request)
} catch {
print(error.localizedDescription)
}
DispatchQueue.main.async(execute: {
self.monosSearchResult = arr
self.tableView.reloadData()
})
}
}