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)
}
Related
I am using NSTableViewDiffableDataSource. When I do a multiple delete, I see the wrong insert-animation after (flicker and moving from top or bottom). How fix it?
// I am using UUID instead of NSManagedObjectID because when I create a new NSManagedObject it first has a temporary objectID.
func configureDataSource() {
let dataSource : NSTableViewDiffableDataSource<String, UUID> = .init(tableView: tableView) { table, column, index, objectID in
let request = NSFetchRequest<Task>()
request.entity = Task.entity()
request.predicate = NSPredicate(format: "id = %#", argumentArray: [objectID])
guard let task = try? self.viewContext.fetch(request).first as? Task else {
return NSView()
}
let cell = self.create(viewFor: column, task: task)
return cell
}
dataSource.defaultRowAnimation = .effectGap
dataSource.sectionHeaderViewProvider = nil
self.dataSource = dataSource
}
func storeDidReloadContent() {
var snapshot = NSDiffableDataSourceSnapshot<String, UUID>()
snapshot.appendSections([""])
snapshot.appendItems(store.objects.compactMap{ $0.id }, toSection: "")
dataSource.apply(snapshot, animatingDifferences: false)
}
func storeDidChangeContent(with snapshot: NSDiffableDataSourceSnapshotReference) {
var newSnapshot = NSDiffableDataSourceSnapshot<String, UUID>()
newSnapshot.appendSections([""])
newSnapshot.appendItems(store.objects.compactMap{ $0.id }, toSection: "")
dataSource.apply(newSnapshot, animatingDifferences: true)
}
// class ObjectFactory
// Batch operation
public func delete(objects: [T]) {
let objectIDs = objects.compactMap{ $0.objectID }
CoreDataStorage.shared.performBackground { privateContext in
objectIDs.forEach{
let object = privateContext.object(with: $0)
privateContext.delete(object)
}
try? privateContext.save()
}
}
PS: Store class (var store) incapsulate all works with NSFetchedResultController.
ObjectFactory class incapsulate all works with NSManagedObjects.
NSFetchedResultController works with only main NSManagedObjectContext.
Batch operation in NSTableView is pain :(
I wrote a code to take data from my CoreDate Entity to show the highest Integer as the value at a Highscore label. I don't understand why it is not working? I tried it with or without a extra function...
func loadHighscore() {
//Kontext identifizieren
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
//Anfrage stellen
let context = appDelegate.persistentContainer.viewContext
let entityName = "PushUps"
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
do {
let results = try context.fetch(request)
for result in results {
guard let count = (result as! NSManagedObject).value(forKey: "highScore") as? Int16 else {
return
}
}
if count > highScore {
highscoreLabel.text = "Highscore: \(count)"
highScoreChanged(newHighScore: Int16(count))
// Console statement:
print("New Highscore: \(count)")
}
} catch {
print("error")
}
}
func highScoreChanged(newHighScore: Int16) {
highscoreLabel.text = "Highscore: \(newHighScore)"
}
}
Your approach is a bit strange.
A better approach is to load the data sorted descending by highScore so the first item is the item with the highest value.
It's highly recommended to take advantage of the generic Core Data types and to use dot notation rather the KVC value(forKey
func loadHighscore() {
//Kontext identifizieren
// delegate can be forced unwrapped. The app doesn't even launch if AppDelegate doesn't exist
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//Anfrage stellen
let context = appDelegate.persistentContainer.viewContext
let entityName = "PushUps"
// Use a specific fetch request
let request = NSFetchRequest<PushUps>(entityName: entityName)
// add a sort descriptor to sort the items by highScore descending
request.sortDescriptors = [NSSortDescriptor(key: "highScore", ascending: false)]
do {
// results is an array of PushUps instances, no type cast needed
let results = try context.fetch(request)
if let result = results.first, result.highScore > highScore {
highScore = result.highScore
print("New Highscore: \(highScore)")
}
} catch {
print(error)
}
highscoreLabel.text = "Highscore: \(highScore)"
}
The function highScoreChanged is not needed either. If the saved highscore is higher than the current value (property highScore) the property is updated and at the end of the method the text field is updated with the value of the property highScore.
Be sure to execute the label update in main queue. In other way it may not be done.
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()
})
}
}
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)
})
}
}
Im working with NSURLSession. I have an array with restaurants and i'm requesting the dishes for every restaurant in the array to the api. The dataTask works,i'm just having a real hard time trying to call a method only when the all dataTasks are finished.
self.findAllDishesOfRestaurants(self.restaurantsNearMe) { (result) -> Void in
if result.count != 0 {
self.updateDataSourceAndReloadTableView(result, term: "protein")
} else {
print("not ready yet")
}
}
the self.updateDataSourceAndREloadTableView never gets called, regardless of my completion block. Here is my findAllDishesOfRestaurants function
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
self.allDishes.addObjectsFromArray(dishesArray as [AnyObject])
print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray)
})
completion(result:allDishesArray)
}
}
}
And here is my the function where i perform the dataTasks.
func getDishesByRestaurantName(restaurant:Resturant, completion:(result:NSArray) ->Void) {
var restaurantNameFormatted = String()
if let name = restaurant.name {
for charachter in name.characters {
var newString = String()
var sameCharacter:Character!
if charachter == " " {
newString = "%20"
restaurantNameFormatted = restaurantNameFormatted + newString
} else {
sameCharacter = charachter
restaurantNameFormatted.append(sameCharacter)
}
// print(restaurantNameFormatted)
}
}
var urlString:String!
//not to myself, when using string with format, we need to igone all the % marks arent ours to replace with a string, otherwise they will be expecting to be replaced by a value
urlString = String(format:"https://api.nutritionix.com/v1_1/search/%#?results=0%%3A20&cal_min=0&cal_max=50000&fields=*&appId=XXXXXXXXXappKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXX",restaurantNameFormatted)
let URL = NSURL(string:urlString)
let restaurantDishesArray = NSMutableArray()
let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithURL(URL!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
do {
let anyObjectFromResponse:AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
if let asNSDictionary = anyObjectFromResponse as? NSDictionary {
let hitsArray = asNSDictionary.valueForKey("hits") as? [AnyObject]
for newDictionary in hitsArray! as! [NSDictionary]{
let fieldsDictionary = newDictionary.valueForKey("fields") as? NSDictionary
let newDish = Dish.init(dictionary:fieldsDictionary!, restaurant: restaurant)
restaurantDishesArray.addObject(newDish)
}
}
completion(result:restaurantDishesArray)
} catch let error as NSError {
print("failed to connec to api")
print(error.localizedDescription)
}
}
dataTask.resume()
}
Like i said before, I need to wait until the fun findAllDishesOfRestaurants is done. I tried writing my completion blocks but I'm not sure I'm doing it right. Any help is greatly appreciated. Thank
The problem is that you are calling the completion method in findAllDishesOfRestaurants before al tasks are complete. In fact, you are calling it once for each restaurant in the list, which is probably not what you want.
My recommendation would be for you to look into NSOperationQueue for two reasons:
It will let you limit the number of concurrent requests to the server, so your server does not get flooded with requests.
It will let you easily control when all operations are complete.
However, if you are looking for a quick fix, what you need is to use GCD groups dispatch_group_create, dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify as follows.
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let group = dispatch_group_create() // Create GCD group
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
dispatch_group_enter(group) // Enter group for this restaurant
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
// self.allDishes.addObjectsFromArray(dishesArray as [AnyObject]) <-- do not do this
// print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray) <-- No need for this, remove
dispatch_group_leave(group) // Leave group, marking this restaurant as complete
})
// completion(result:allDishesArray) <-- Do not call here either
}
}
// Wait for all groups to complete
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(result:allDishesArray)
}
}