Saving Realm objects on interactive UI - ios

I have a table view and once a cell is about to become visible I load its image. When the image is loaded a model holding its NSData (which inherits from Object) should be written to a database.
I have tried two ways:
Wait until all images are loaded and then write the data to the
database.
Write each loaded image's model to the database.
The first one requires either to scroll through the whole table view (if we load images lazily) or to load images on viewDidLoad() which isn't the best choice either.
The second way is good, as soon as an image is loaded its model eventually updates. But Realm freezes the UI on write() function.
I've tried to use an asynchronous main queue for writing but this produces short glitches each time Realm performs the write operation. I also tried to use a UserInitiated asynchronous queue but this only caused my app to crash...
let queue = NSOperationQueue()
queue.qualityOfService = .UserInitiated
// this code is executed on imageDidLoad()
queue.addOperationWithBlock {
let realm = Realm()
realm.refresh()
realm.write {
realm.add(downloadable!, update: true)
}
}
As a result I'm getting:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Object is already persisted in a Realm'
What a solution might be?

It's hard to tell without more context, but it appears that downloadable is already a persisted Object, so trying to use it on another thread won't actually work. If downloadable has a primary key, you might want to use that instead, doing something akin to the following:
let queue = NSOperationQueue()
queue.qualityOfService = .UserInitiated
let primaryKey = downloadable.primaryKey
// this code is executed on imageDidLoad()
queue.addOperationWithBlock {
let realm = Realm()
realm.refresh()
realm.write {
realm.create(DownloadableClass.Self, value: ["primaryKey" : primaryKey, "imageData" : imageData], update: true)
}
}

Related

Interacting with realm database on background thread

I believe I should be able to work with a realm database on a background thread in an iOS app as long as I don't pass realm objects around that was created on another thread. That being said, I am experiencing the following error:
'RLMException', reason: 'Realm accessed from incorrect thread.'
The code where I'm trying to do my realm operation is in an observer off of the AVPlayer. I have the observer setup to run on a background queue as shown below:
backgroundStateObserver = self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(5, preferredTimescale: 1), queue: DispatchQueue.global(qos: .background)) { (CMTime) -> Void in
if self.player.currentItem?.status == .readyToPlay {
let realm = try! Realm()
let predicate = NSPredicate(format: "episode_id = %# AND user_id = %#", self.currentEpisode!.id, userId)
var episode = realm.objects(ListenedToEpisode.self).filter(predicate).first
...
}
The error is raised right after the "let realm" statement. Is what I am trying to do not allowed? If I put it inside a Dispatch.main.async statement, it works, but that seems like I shouldn't need to do that unless I'm misunderstanding what realm requires. Please help set me straight.

CoreData error: API Misuse: Attempt to serialize store access on non-owning coordinator

Note:
This post does not apply since I actually use CoreData.
In this post the last answer suggests to fetch all items in the new background thread before adding new objects, but this is done in my code.
This post suggests to unfault an item before its context is saved, but this is also done in my code.
My app uses CoreData to store objects called shoppingItems. I wrote a class CoreDataManager that initialises CoreData and has essentially one function to overwrite the currently stored items, and one function to fetch all items. Both functions operate in the background, i.e. on a separate thread.
Here is my code (irrelevant parts are left out).
I setup core data on the main thread:
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
})
return container
}()
This is the write function:
func overwriteShoppingItems(_ shoppingItems: Set<ShoppingItem>, completion: #escaping (Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
// Delete currently stored shopping items
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: CDEntityShoppingItem)
do {
let result = try backgroundContext.fetch(fetchRequest)
let resultData = result as! [NSManagedObject]
for object in resultData {
backgroundContext.delete(object)
}
if !shoppingItems.isEmpty {
// Save shopping items in managed context
let cdShoppingItemEntity = NSEntityDescription.entity(forEntityName: CDEntityShoppingItem, in: backgroundContext)!
for nextShoppingItem in shoppingItems {
let nextCdShoppingItem = CDShoppingItem(entity: cdShoppingItemEntity, insertInto: backgroundContext)
nextCdShoppingItem.name = nextShoppingItem.name
}
}
let saveError = self.saveManagedContext(managedContext: backgroundContext)
completion(saveError)
} catch let error as NSError {
completion(error)
}
}
}
func saveManagedContext(managedContext: NSManagedObjectContext) -> Error? {
if !managedContext.hasChanges { return nil }
do {
try managedContext.save()
return nil
} catch let error as NSError {
return error
}
}
And this is the fetch function:
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundContext.fetch(fetchRequest)
guard !cdShoppingItems.isEmpty else {
completion([], nil)
return
}
for nextCdShoppingItem in cdShoppingItems {
}
completion(shoppingItems, nil)
} catch let error as NSError {
completion(nil, error)
}
}
}
In normal operation, the code seems to work.
The problem:
I also wrote a unit test that tries to provoke multi-threading problems. This test uses a concurrent dispatch queue:
let concurrentReadWriteQueue = DispatchQueue(label: „xxx.test_coreDataMultithreading", attributes: .concurrent)
A timer defines the test time.
In the scheme for tests, I have set the arguments -com.apple.CoreData.Logging.stderr 1 and -com.apple.CoreData.ConcurrencyDebug 1.
During the test time, overwriteShoppingItems and fetchShoppingItems are inserted repeatedly in the queue, and executed concurrently.
This unit test executes a few reads and writes, before it stops at the line
let itemName = nextCdShoppingItem.name!
because nextCdShoppingItem.name is nil, which should never happen, because I never store nil.
Immediately before the crash, the following is logged:
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600000e6c980, store PSC = 0x0)
If I do only fetches, or only writes, the CoreData warning is not logged. Thus it seems definitely to be a multi-threading issue.
However, CoreData.ConcurrencyDebug does not detect it.
It looks as if during a fetch operation on one thread, the other thread deletes the currently fetched item, so that its properties are read back as nil.
But this should not happen because fetches and saves are done with backgroundContext.performAndWait, i.e. serially.
And the stack trace shows that only a single thread accesses CoreData: Thread 3 Queue : NSManagedObjectContext 0x600003c8c000 (serial)
My questions:
What is my misuse of the CoreData API, and how to do it right?
Why is an item sometimes read back as nil?
EDIT:
Maybe this helps to identify the problem: When I comment out backgroundContext.delete(object) in overwriteShoppingItems, the error is no longer logged, and no item is fetched as nil.
Okay so i got same error but only while "archiving" the project. Took me several hours to find the issue, since I was getting the error only on uploading, in the assets catalog, I had some duplicate images, by removing them the error is gone.
Anyone else getting this error AND your Coredata setup is fine, check your assets folder.
If you are not using core data in your project then check your assets files are proper or not.
I am also getting error like this then i have findout this error from my project.
Problem seems to be solved.
It happened apparently, because the functions overwriteShoppingItems and fetchShoppingItems both setup a separate background context with let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) with their own queue so that fetches and saves were not serialized by a single queue.
I modified my code now in the following way:
I have now a property
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
that is initialized as
self.backgroundContext.persistentStoreCoordinator = self.persistentContainer.persistentStoreCoordinator
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
and fetches and saves are done using
backgroundContext.perform {…}
Now the CoreData error is no longer logged, and no item is fetched as nil.
In my case I created an new managedObjectContext without (yet) assigning a persistent store. Eventually I called -[psc metadataForPersistentStore:store] with the store being nil. This caused the log message to be written.
(side-note: you can always catch these outputs by setting a breakpoint on fprintf)
If you are seeing this during tear-down of a Core Data stack, check for un-removed observers or memory leaks.
In my case, I had managed objects which were each observing themselves – one of their relationships, using Swift's Key-Value Observing (NSKeyValueObservation objects). (Example use case: When the last a department's employees are deleted, delete the department.) The console messages appeared while the containing Core Data document was being closed. By breaking on fprintf I could see that the message was logged when a notification was being sent to an observer, and the number of messages was always equal to the number of non-faulted such objects in the managed object context (5 departments --> 5 console messages, etc., etc.). The problem was that I was not removing these observations, and the solution was of course to implement func willTurnIntoFault() in the observing objects, and in this func remove the observation.
In another situation, these messages stopped after I fixed a memory leak, but I did not study that in detail.
The full error messages were: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600002c5cc00, store PSC = 0x0)
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600002c5cc00, store PSC = 0x0) and they occurred during the call to NSPersistentStoreCoordinator.remove(_:). So, this error is telling me that the store's PSC was nil, but I checked this with the debugger and found that the store's PSC was not nil before the call. I filed a bug to Apple on this (FB11669843) asking if they could improve the error message.

RLMException attempting to create object with an existing primary key after check

I'm receiving an RLMException for the following reason:
Attempting to create an object of type 'Student' with an existing
primary key value '258975085-504336622-62850'.
The confusing part is that it's occurring just after a check that there are no existing objects with this key in the Realm.
let realm = try Realm()
if let info = realm.object(ofType: Student.self, forPrimaryKey: newStudent.userId) {
try realm.write {
info.name = newStudent.name
info.school = newStudent.school
info.email = newStudent.email
}
}
else {
try realm.write {
realm.add(newStudent) //RLMException occurs here
}
}
This code is all running asynchronously on the GCD utility queue, inside a do/catch block. It's triggered by a button in the user interface, but nothing else is accessing realm at the same time.
Why could that if statement allow the else code to run?
try! self.realm.write {
self.realm.add(newStudent, update: true)
}
You're adding same Object (student) with existing primary key. So you can just update current one. Instead of deleting and adding new one.
In my case, I added condition to check whenever new user logs in:
if newStudent == nil{
self.realm.add(newStudent, update: .all)
}
Answering my own question because I found the problem.
I think the problem was a previous app screen trying to save (the same) student object on a queue with Utility quality of service, meaning that it finished saving after the call to realm.object(...), but before the call to realm.add(...).
Moving the if statement inside the realm write transaction also helped (thanks EpicPandaForce).

Realm - database isn't up to date after a delete

I've got some objects in my Realm database. I need to delete an object from it.
When I delete my object from my Realm() instance, the objects are well deleted.
But after performing the delete request, I need to retrieve all objects from the database, but here surprise, the object is still here.
I think I have a thread problem, or something like that.
I don't know where to investigate.
My simplified code :
My delete method :
func myDeleteFunc(completion : ()->Void){
let realm = try! Realm()
// ...
try! realm.write {
realm.delete(myObject)
}
completion()
}
// Here the object have been deleted from the realm instance, no problem
This method is called from a viewController where I execute the completion block.
This completion block contains the request that retrieve all objects from my Realm database :
The viewController that executes the method and the completion block :
myDeleteFunc(completion: {
DispatchQueue.main.async {
let realm = try! Realm()
let objects = Array(realm.objects(MyObject.self).sorted(byProperty: "aProperty"))
// Here objects still contains the object that I have already deleted
// ...
}
}
I think my 2 realm instances differs, or have problems between threads because I have a DispatchQueue.main.async.
Any ideas ?
EDIT :
I noticed that when I check with breakpoints, sometimes it works.
So maybe that the delete request have not been committed yet, and that I retrieve the objects before the end of the delete request ?
Make sure you put the async block in an autorelease pool:
myDeleteFunc(completion: {
DispatchQueue.main.async {
let realm = try! Realm()
let objects = Array(realm.objects(MyObject.self).sorted(byProperty: "aProperty"))
// Here objects still contains the object that I have already deleted
// ...
}
}
Should be
myDeleteFunc(completion: {
DispatchQueue.main.async {
autoreleasepool {
let realm = try! Realm()
let objects = Array(realm.objects(MyObject.self).sorted(byProperty: "aProperty"))
// Here objects still contains the object that I have already deleted
// ...
}
}
}
Make sure you do this autoreleasepool { ... } wrap for any background thread where you create a Realm instance, primarily in the GCD.
If that still doesn't work, you can do:
myDeleteFunc(completion: {
DispatchQueue.main.async {
autoreleasepool {
let realm = try! Realm()
realm.refresh()
Realm isolates transactions on each thread to avoid changes from one thread immediately affect another. This mechanism also avoids the "faults" inherent to ORMs.
In your code, you can choose to refresh (advance) the realm to the latest state at points that you control and can handle data before and after the refresh being different.
Realms on a thread with a runloop (such as the main thread) auto-advance on every iteration of the runloop by default.
In your code sample, you invoke DispatchQueue.main.async immediately after a commit from another thread, which means that if you already have a Realm on the main thread, the async block will be at the same state and won't include the last commit.
But you can call Realm.refresh() explicitly at the start of your async block, which will ensure that this block sees that last commit:
myDeleteFunc(completion: {
DispatchQueue.main.async {
let realm = try! Realm()
realm.refresh()
let objects = Array(realm.objects(MyObject.self).sorted(byProperty: "aProperty"))
// ...
}
}
See Realm's docs on Seeing Changes From Other Threads for more information.

Realm Threading Confusion

So I'm working on setting up a background queue that does all realm writes on its own thread. I've run into some strange issues I can't figure out.
Issue #1
I'm not sure if this is related (see post: Xcode debug issues with realm) but I do have an apparent mismatch with my lldbg output as to whether a certain field:
messages element
My DataTypes
OTTOSession
class OTTOSession : Object {
dynamic var messages : MessageList?
dynamic var recordingStart : Double = NSDate().timeIntervalSince1970
func addLocationMessage(msg : dmParsedMessage) -> LocationMessage {
let dmsg : dmLocationMessage = msg as! dmLocationMessage
let locMsg = LocationMessage(locMsg: dmsg)
self.messages!.locationMessages.append(locMsg)
return locMsg;
}
}
MessageList
public class MessageList : Object {
dynamic var date : NSDate = NSDate();
dynamic var test : String = "HI";
let locationMessages = List<LocationMessage>()
let ahrsMessages = List<AHRSMessage>()
// let statusMessages = List<StatusMessageRLM>()
let logMessages = List<LogMessage>()
}
Realm Interactions
In my code I create my new OTTOSession in a code block on my realmQueue
internal var realmQueue = dispatch_queue_create("DataRecorder.realmQueue",
DISPATCH_QUEUE_SERIAL)
All realm calls are done on this realmQueue thread
dispatch_async(realmQueue) {
self.session = OTTOSession()
}
I've also tried different variants such as:
dispatch_async(realmQueue) {
self.session = OTTOSession()
// Directly making a message list
self.session!.messages = MessageList()
//Making a separate message list var
self.messages = MessageList()
self.session!.messages = self.messages
}
The reason I've played around with the MessageList is that I cant tell from the debugger whether the .messages variable is set or not
Recording
Once I signal to my processes I want to start recording I then actually make the write calls into Realm (which I'm not 100% sure i'm doing correctly)
dispatch_async(realmQueue){
// Update some of the data
self.session!.recordingStart = NSDate().timeIntervalSince1970
// Then start writing the objects
try! Realm().write {
// I've tried different variants of:
let session = self.session!
try! Realm().add(self.session!)
// Or
try! Realm().add(self.session!)
// or
let session = self.session!
session.messages = MessageList()
session.messages!.ahrsMessages
try! Realm().add(self.session!)
try! self.session!.messages = Realm().create(MessageList)
try! Realm().add(self.session!.messages!)
print ("Done")
}
}
Basically I've tried various combinations of trying to get the objects into realm.
Question: When adding an object with a one-to-one relationship do I have to add both objects to Realm or will just adding the parent object cause the related object to also be added to realm
Adding Data
Where things start to go awry is when I start adding data to my objects.
Inside my OTTOSession Object I have the following function:
func addLocationMessage(msg : dmParsedMessage) -> LocationMessage {
let dmsg : dmLocationMessage = msg as! dmLocationMessage
let locMsg = LocationMessage(locMsg: dmsg)
// THIS LINE IS CAUSING A 'REALM ACCESSED FROM INCORRECT THREAD ERROR
self.messages!.locationMessages.append(locMsg)
return locMsg;
}
I'm getting my access error on this line:
self.messages!.locationMessages.append(locMsg)
Now the function call itself is wrapped in the following block:
dispatch_async(realmQueue) {
try! Realm().write {
self.session?.addLocationMessage(msg)
}
}
So as far as I can tell by looking at the debugger - and by looking at the code - everything should be running inside the right thread.
My queue is SERIAL so things should be happening one after another. The only thing I can't figure out is when I break at this point the debugger does show that messages is nil but I cant trust that because:
Question
So my question is two fold
1) Is my code for adding an object into the RealmDB correct. i.e. do I need to make two separate Realm().add calls for both the OTTOSession and the MessageList or can I get away with a single call
2) Is there anything that pops out to explain why I'm getting a thread violation here - should doing all my realm writing calls on a single thread be enough ?
1) No, you don't need to make two separate calls to Realm.add(). When you add an object to a Realm all related objects are persisted as well.
2) Your thread violation very likely originates from the fact that dispatch queues make no guarantee over the thread on which they are executed on (beside the main queue). So that means your Realm queue is executed on different threads. You will need to make sure to retrieve your session object from a Realm opened on this thread. You might want to use primary keys for that purpose and share those between queues / threads.

Resources