reload table with async search result - swift

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()
})
}
}

Related

Core data does not save the data properly

EDITED
I have a UITableView which displays multiple social media posts. I use the Prefetch Source Delegate for prefetching Posts and I use Core Data to be stored after being fetched from web server. Problem that I have is that I get no error, but the data does not stay saved between launches in CoreData.
Snip of code
func configureDataSourceTable(){
self.dataSource = UITableViewDiffableDataSource<String,String>(tableView: self.table_view){
(tableView, indexPath, ref) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CheckInCell
if !self.postData.currentPostFetching(ref: ref){
self.postData.fetchPost(ref: ref) { post in
DispatchQueue.main.async {
self.setPostNeedsUpdate(ref: ref)//reconfigure items
}
return cell
}
func fetchPost(ref: String,completion : (PostStruct)->Void) {
if self.dataContainer.postIsPartOfCoreData(ref: ref){
var postStruct = self.dataContainer.getPostStructFromDB(ref: ref)
completion(postStruct)
}else{ //has to be downloaded from web server
Task {
let post = await getPostFromServer()//not relevant in understanding problem
var postStruct = self.convertDataToPost(post)
completion(postStruct)
self.dataContainer.savePostStruct(post: postStruct)
}
}
}
Class of the DataContainer subclass of NsPersistentStore
func savePostStruct(post_struct : PostStruct,image_data : String){
Task.detached {
var postObject = PostCore.init(context : self.viewContext)
postObject.name = "Test"
var image = ImageCore.init(context: self.viewContext)
imageObject.data = image_data
postObject.image = imageObject
do {
if Thread.isMainThread {
print("Running on the main thread parent")
}else {
print("Other thread")
}
try self.viewContext.save()
print("Reference \(post_struct.ref) child saved")
}catch {
print("Error that produced \(post.ref) catched while trying to savethe data on child: \(error.localizedDescription),number : \(error)");
}
if post_struct.ref == "AAAA"{
var post = PostCore.fetchRequest()
var predicate = NSPredicate.init(format: "ref like %#", post.ref!)
fetchCheckIn.predicate = predicate
fetchCheckIn.fetchLimit = 1
fetchCheckIn.includesPropertyValues = true
let result = try! self.viewContext.fetch(fetchCheckIn)
print(result)
//This line returns the Object
print(self. checkPostExistHasPictures(refPost: post.ref))
//This is the line where always come (false, false) meaning that the Post is not saved
}
}
func getPostStructFromDB(refPost : String)async->PostStruct {
return await self.viewContext.perform{
var fetchPost = PostCore.fetchRequest()
var predicate = NSPredicate.init(format: "ref like %#", refPost)
fetchPost.predicate = predicate
fetchPost.fetchLimit = 1
fetchPost.includesSubentities = true
fetchPost.returnsObjectsAsFaults = false
fetchPost.includesPropertyValues = true
let result = try? self.viewContext.fetch(fetchCheckIn)
var refPost = result.first.ref
return PostStruct(ref : ref, image: refPost.image.data)
}
}
}
func checkPostExistHasPictures(refPost : String)->(Bool,Bool){
var fetchCheckIn = CheckInCore.fetchRequest()
var predicate = NSPredicate.init(format: "ref like %#", refPost)
fetchCheckIn.predicate = predicate
fetchCheckIn.fetchLimit = 1
var exist = false
var hasPicture = false
self.viewContext.performAndWait {
do{
let result = try? self.viewContext.fetch(fetchCheckIn)
if result?.first == nil {
}else {
print("Exists with reference \(reference_checkin)")
if result!.first!.pic_ref == nil {
exist = true
hasPicture = false
}else if result!.first!.image!.count != 0 {
exist = true
hasPicture = true
}
}
}catch {
print("error catched")
}
}
return(exist, hasPicture)
}
}
Relation between PostCore and ImageCore is zero to many.
I don't get any error code or error message. I commented the line where I get the error. I have tried all possible ways using a backGroundContext each time a save is made to not block the main thread and still is the same problem.
Your code is extremely confusing.
Why have you got Task scattered everywhere, you are doing no async/await work?
Your savePostStruct method takes a parameter called post which contains your data, then you immediately replace it with a value of the same name of type PostCore, which is presumably a managed object, then you only set one value on it.
Then, when you come to fetch an item, there's nothing there, because you haven't written anything to the managed object besides the name "Test".
At the very least, you have to change this line:
var post = PostCore.init(context : self.viewContext)
To
let postObject = PostCore(context: self.viewContext)
Then you won't get confused between the managed object and the struct you're passing in.
You are also saving the context before you've written any of the values to it.

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)
}

CloudKit how to modify existing record (swift 3)

How can I modify an existing CloudKit record?
I receive a record from CloudKit with this code:
let name = tmpVar as! String
let container = CKContainer.default()
let privateDatabase = container.privateCloudDatabase
var predicate = NSPredicate(format: "email == %#", name)
var query = CKQuery(recordType: "MainTable", predicate: predicate)
privateDatabase.perform(query, inZoneWith: nil) { (results, error) -> Void in
if error != nil {
pass
}
else {
if (results?.count)! > 0 {
for result in results! {
self.likedArr.append(result)
}
if let checker = self.likedArr[0].object(forKey: "like") as? String {
print ("CHEKER IS \(checker)")
let intChecker = Int(checker)
let result = intChecker! + 1
} else {
print ("EMPTY")
}
} else {
print ("Login is incorrect")
}
OperationQueue.main.addOperation({ () -> Void in
// self.tableView.reloadData()
// self.tableView.isHidden = false
// MBProgressHUD.hide(for: self.view, animated: true)})}
and how to return it back modified value of "like" key to the owner "name"?
When you get the records from the cloud, you can cast them to CKRecords.
In this CKRecord object you just modify the values you want to update, and then save it all again to the cloud. The CKRecordId must be the same, otherwise you'll just make a new record.
here is how to modify the records:
MyCKRecord.setValue(object, forKey: "myKey")
When you call the query, you get an array of CKRecord objects. Use the subscript to edit the record:
record["key"] = value as CKRecordValue
when you're finished, take the CKRecord and use either CKModifyRecordsOperation or CKDatabase.save(_:completionHandler:) to save it back to the server.
Sharing my solution:
self.likedArr[0].setValue(1, forKey: "like")
let saveOper = CKModifyRecordsOperation()
saveOper.recordsToSave = self.likedArr
saveOper.savePolicy = .ifServerRecordUnchanged
saveOper.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if saveOper.isFinished == true {
}
}
privateDatabase.add(saveOper)

Cannot convert value of type 'String?!' to expected argument type 'Notifications'

I am trying to check the id of a record before I put it into the array, using xcode swift
here is the code. But, i get the following error
Notifications.swift:50:46: Cannot convert value of type 'String?!' to expected argument type 'Notifications'
on this line
*if (readRecordCoreData(result["MessageID"])==false)*
Please can some one help to explain this error
import CoreData
struct Notifications{
var NotifyID = [NSManagedObject]()
let MessageDesc: String
let Messageid: String
init(MessageDesc: String, Messageid:String) {
self.MessageDesc = MessageDesc
self.Messageid = Messageid
// self.MessageDate = MessageDate
}
static func MessagesWithJSON(results: NSArray) -> [Notifications] {
// Create an empty array of Albums to append to from this list
var Notification = [Notifications]()
// Store the results in our table data array
if results.count>0 {
for result in results {
//get fields from json
let Messageid = result["MessageID"] as! String
let MessageDesc = result["MessageDesc"] as? String
let newMessages = Notifications(MessageDesc: MessageDesc!, Messageid:Messageid)
//check with id's from core data
if (readRecordCoreData(result["MessageID"])==false)
{
Notification.append(newMessages)
}
}
}
return Notification
}
//check id
func readRecordCoreData(Jsonid: String) -> Bool {
var idStaus = false
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let fetchRequest = NSFetchRequest(entityName: "ItemLog")
//3
do {
let resultsCD = try! managedContext.executeFetchRequest(fetchRequest)
if (resultsCD.count > 0) {
for i in 0 ..< resultsCD.count {
let match = resultsCD[i] as! NSManagedObject
let id = match.valueForKey("notificationID") as! String
if (Jsonid as String! == id)
{
idStaus = true
}
else{
idStaus = false
}
}
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
return idStaus
}
One of your methods is static and the other one is not :
func readRecordCoreData(Jsonid: String) -> Bool
static func MessagesWithJSON(results: NSArray) -> [Notifications]
Depending on what you want to accomplish you could declare both static, none, or replace
//check with id's from core data
if (readRecordCoreData(result["MessageID"])==false)
{
Notification.append(newMessages)
}
By
//check with id's from core data
if (Notifications.readRecordCoreData(Messageid)==false)
{
Notification.append(newMessages)
}
Not sure if the code will work past compilation however as there are many readability issues

Swift CloudKit QueryOperation Duplicates

When I perform a query operation, despite only having 501 records in the cloudkit dashboard, I get around 1542 results (all duplicates).
This is my code:
func queryForTable() -> Void {
self.arrayOfFoodItems.removeAllObjects()
let container = CKContainer.defaultContainer()
let resultPredicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "FoodItems", predicate: resultPredicate)
let queryOp = CKQueryOperation(query: query)
let operationQueue = NSOperationQueue()
executeQueryOperation(queryOp, onOperationQueue: operationQueue)
}
func executeQueryOperation(queryOperation: CKQueryOperation, onOperationQueue operationQueue: NSOperationQueue){
queryOperation.database = CKContainer.defaultContainer().publicCloudDatabase
queryOperation.recordFetchedBlock = self.addRecordToArray
queryOperation.queryCompletionBlock = { (cursor: CKQueryCursor?, error: NSError?) -> Void in
if cursor != nil {
if let queryCursor = cursor{
let queryCursorOperation = CKQueryOperation(cursor: queryCursor)
self.executeQueryOperation(queryCursorOperation, onOperationQueue: operationQueue)
}
}
else {
self.sortToSectionsAndReloadData()
}
}
operationQueue.addOperation(queryOperation)
}
How do I solve this problem? Thanks a lot!
UPDATE: Here's the other 2 functions I'm using. As stated in the comments, I'm calling queryForTable() in viewDidLoad.
func sortToSectionsAndReloadData() {
for (var i = 0; i < self.arrayOfSections.count; i++) {
self.arrayOfArrays[i].removeAllObjects()
let prefix:String = self.arrayOfSections[i]
let array:NSMutableArray = self.arrayOfArrays[i] as! NSMutableArray
for object in self.arrayOfFoodItems {
let name = object["itemName"] as! String
if name.lowercaseString.hasPrefix(prefix.lowercaseString) {
array.addObject(object)
}
}
}
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in
self.tableView.reloadData()
}
}
func addRecordToArray (record: CKRecord!) {
self.arrayOfFoodItems.addObject(record)
let recordItemName = record["itemName"]
print("\(recordItemName)")
}
With each queryCompletionBlock you will receive all record that are fetched up to the cursor. So the first query you will get a result of about 100 records, then 200, then 300, then 400 and then 500. In your case you add those to your results each time. If you add these up, then you end up with 1500 records. So instead of adding the results to your data array you should replace the data array with the results.
I would venture that the queryForTable() function is getting called again before the previous call has completed its query operation. You would get parallel queries feeding your array, which gets reinitialized by the last queryForTable() but still receives data from the ongoing queries that have not yet finished receiving data.