Periodically save an object in Realm - ios

I have a Realm object called Trip. It stores data of a user's movement.
class Trip: Object {
dynamic var id: Int = 0
dynamic var startTimestamp: Int64 = 0
dynamic var endTimestamp: Int64 = 0
dynamic var distance: Double = 0.0
dynamic var calories: Double = 0.0
dynamic var averageSpeed: Double = 0.0
}
In the view controller, I keep a class-level variable called trip.
fileprivate var trip: Trip?
Whenever a user starts a trip, I initialize a Trip object and assigns it to this variable.
trip = Trip()
And throughout the user's movements, I keep updating this trip object with the data.
I need to save this data to the Realm database every 1 minute. So I run a timer.
Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(save()), userInfo: nil, repeats: true)
Which executes a function to save this object in a background thread.
fileprivate func save() {
do {
DispatchQueue(label: "RealmBackgroundThread").async {
autoreleasepool {
let realm = try! Realm()
try! realm.write {
realm.add(self.trip!, update: true)
}
}
}
} catch {
}
}
Up to here, it works fine. The problem is after the first save, when I try to access that trip object again, it crashes with the following error.
libc++abi.dylib: terminating with uncaught exception of type realm::IncorrectThreadException: Realm accessed from incorrect thread.
I think this happens because I open a new Realm to save this object in a background thread. I know that Realm isn't thread-safe.
But I'm not sure how to resolve this. How do I keep using the same trip object after saving it?

To quote from Realm's documentation about Passing Instances Across Threads:
Instances of Realm, Results, or List, or managed instances of Object are thread-confined, meaning that they can only be used on the thread on which they were created, otherwise an exception is thrown.
In your case, self.trip is a managed instance of an Object subclass, and you appear to be accessing it from both the main thread, and repeatedly from the serial dispatch queue you create within your save() method. It's important to keep in mind that the call to DispatchQueue.async will result in your code being executed on different threads depending on the whims of Grand Central Dispatch. That is to say that two consecutive calls like …
DispatchQueue(label: "RealmBackgroundThread").async {
// work
}
… will often result in the work being performed on two different threads.
Realm's documentation on Passing Instances Across Threads contains an example of how to pass a thread-safe reference to an object across threads and resolve it in the Realm instance on the target thread.
It's hard to provide a more specific suggestion in your case as I'm not clear what you're trying to do with your save() method and why you're calling it on a timer. Managed Object instances (instances retrieved from a Realm, or that have already been added to a Realm) can only be modified within a write transaction, so any modifications to self.trip would need to be performed within the write transaction opened by your save() method for it to serve any purpose. For the pattern of using a timer to make sense you'd need to leave self.trip as an unmanaged object, at which point save() would update the Trip instance with the same ID in the Realm file. To do this you'd want to use Realm.create(_:value:update:) instead of Realm.add(_:update:), as create does not convert unmanaged instances to managed instances like add does.

Related

Why do we need mainQueueConcurrencyType in Core Data?

I'm a newbie in Core Data and get confused about which concurrency queue type I should specify when creating a NSManagedObjectContext. When do we want to use mainQueueConcurrencyType? Here is how Apple documented:
Private queue
(NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType):
The context creates and manages a private queue.
Main queue
(NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType): The
context is associated with the main queue, and as such is tied into
the application’s event loop, but it is otherwise similar to a private
queue-based context. You use this queue type for contexts linked to
controllers and UI objects that are required to be used only on the
main thread.
I know there is a NSFetchedResultsController that can be associated with a UITableView, but I'm not going to use this controller. Can anyone show me an example of how to use mainQueueConcurrencyType?
From my understanding, assume we have a NSManagedObject subclass called CoreDataDesk and it will be used to draw desk objects on screen like this:
class CoreDataDesk: NSManagedObject {
var color: UIColor?
}
class DeskView: UIView {
// Create a MOC with mainQueueConcurrencyType first.
// Then fetch a bunch of CoreDataDesk objects to initialize DeskView objects.
required init(desk: CoreDataDesk) {
self.backgroundColor = desk.color
super.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
}
}
Is this a correct example of using main concurrency type context to fetch CoreDataDesk objects and draws UI with them? If so, I think we can do fetch CoreDataDesk with private concurrency type context instead, then we initialize another Desk class(which is not NSManagedObject subclass) with CoreDataDesk still in background thread. Finally we switch back to main thread and use Desk to initialize DeskView as follows:
class CoreDataDesk: NSManagedObject {
var color: UIColor?
}
class Desk {
var color: UIColor?
init(coreDataDesk: CoreDataDesk) {
color = coreDataDesk.color
}
}
func fetchCoreDataDesk() {
// Create a MOC with privateQueueConcurrencyType first.
// Then fetch a bunch of CoreDataDesk objects.
DispatchQueue.main.async {
// convert each CoreDataDesk to Desk
}
}
You can do everything with private queue concurrency. You can also pretty much do everything with main queue concurrency. They're different options for different needs and programming styles. Your approach would work but it's not the only way.
Managed objects are not thread safe, so if you fetch one, you need to use it on the same thread where it was fetched. If you plan to use it on the main queue-- for example to update the UI-- then it must be fetched on the main queue. Main queue concurrency supports this. This is true whether or not you use NSFetchedResultsController.
If you want to write a bunch of code to convert between managed objects and non-managed objects or structs (your Desk and CoreDataDesk, for example), that might not be necessary-- if your non-managed objects are thread safe. Core Data doesn't require you to do that, so it's up to you to decide whether to accept the extra complexity in order to avoid main queue concurrency.

Does managedObjectContext.object(with:) always refetch data if another (private) managedObjectContext changed and saved it?

(I'm sorry if this question is kind of confusing/imprecise. I'm just learning advanced CoreData usage and I don't know the terminology and stuff very well).
I have a singleton Game that holds certain data you need during the game. For example, you can access currentSite (Site is a CoreData Entity) from there to get the Site the user is currently at:
// I created the Site in a background queue (when the game started), then saved the objectID and here I load the objectID
public var currentSiteObjectID: NSManagedObjectID {
let objectIDuri = UserDefaults.standard.url(forKey: Keys.forGameObject.currentSiteObjectIDURI)!
return appDelegate.persistentContainer.persistentStoreCoordinator.managedObjectID(forURIRepresentation: objectIDuri)!
}
// managedObjectContext is the one running on the main queue
public var currentSite: Site! {
return managedObjectContext.object(with: currentSiteObjectID) as! Site
}
You see, I retrieve the currentSite by using the managedObjectContext.object(with:) method.
The documentation of this method says:
Returns the object for a specified ID.
If the object is not registered
in the context, it may be fetched or returned as a fault. (...)
I'm not quite sure about the following:
// Each Site has resources that you can access like this
print(Game.shared.currentSite!.resourceSet!.iron)
appDelegate.persistentContainer.performBackgroundTask { (context) in
let currentSite = context.object(with: Game.shared.currentSiteObjectID) as! Site
// Here I increase the iron resource
currentSite.resourceSet!.iron += 42
do {
try context.save()
} catch let error as NSError {
fatalError("\(error.debugDescription)")
}
DispatchQueue.main.async {
print(Game.shared.currentSite!.resourceSet!.iron)
}
}
The second print function is using the managedObjectContext of the main queue (which is different to the private one used inside performBackgroundTask {...}).
It actually does print:
50 // the start value
92
My question: Is it guaranteed that managedObjectContext.object(with:) returns the current object (that is up-to-date), even if it has been changed in another context? The documentation says that it will be fetched if it's a new object that's not known to the context.
But what if an object changes?
I'm not sure if it's just a coincidence that the example code from above is working like expected.
Thanks for any help/explanation! I'm eager to learn about this kind of stuff.
No it is not guaranteed. If managed object is already registered in context then it will return this object. What's more, if object with given id (NSManagedObjectId) doesn't exist in persistent store then your app will crash as soon as you try to use any of its properties.

Initialize an empty Results<T> variable

I have a Realm object called Task. I'm displaying these tasks in a table view. I have a variable decalred to hold these objects.
var tasks: Results<Task>!
At initial launch, I'm getting these tasks from an API. Until the local realm is empty. But still when the UITableViewController loads, it fires the data source methods. At this point, the tasks variable can still be nil. So the app crashes at methods like numberOfRowsInSection.
How can I initialize the above variable so that it can be in an empty state and it won't cause crashes?
So the app crashes at methods like numberOfRowsInSection
Don't let it. You are the one who is causing the crash, by assuming that tasks is not nil when in fact it can be. It is your job to check in numberOfSections to see whether tasks is nil. If it is, return 0 so you don't get asked any other questions.
The following code will demonstrate the way how to initialize an empty Results object.
class TaskManager {
private var _realm: Realm?
var realm: Realm {
if _realm == nil {
_realm = try! Realm()
}
return _realm!
}
var tasks: Results<Task> = realm.objects(Task.self).filter("FALSEPREDICATE")
}

Object passed by reference will not exist. Swift

I have an array.
var array:[customType] = [] // pseudo code
func Generate_New_Array(){
//initialization of generatedNewArray
array = generatedNewArray
for (index,element) in array{
async_process({
Update_Data_From_Web(&array[index])
})
}
})
}
func Update_Data_From_Web(inout object:customType){
download_process{
object = downloadedData
}
}
The question is , what will should I do if I call Generate_New_Array before Update_Data_From_Web will finish for each of elements. They will store value back to not-existing index in array. How to avoid problems with that.
You have a couple of options:
Make the Generate_New_Array process cancelable, and then cancel the old one before starting the new one.
Make the Generate_New_Array serial so that when you make a subsequent call to this method, it will finish the calls first. For example, you could have this enqueue an operation on a serial queue.
Regardless of which approach you adopt, if this is multithreaded code, make sure you synchronize your interaction with the model object (via GCD queues or locks or whatever).

Error creating a separate NSManagedObjectContext

Before getting into my issue, please have a look at this image.
Here is the actual data model:
I retrieve a set of Records from a web API, create objects out of them, save them in core data and display them in the Today view. By default these records are returned for the current date.
The user can tap on Past button to go to a separate view where he can choose a past or future date from a date picker view and view Records for that selected date. This means I have to call the API again passing the selected date, retrieve the data and save that data in core data and display them. When the user leaves this view, this data should be discarded.
This is the important part. Even though I get a new set of data, the old original data for the current date in the Today view must not go away. So if/when the user returns to the Today view, that data should be readily available as he left it without the app having to call the API and get the data for the current date again.
I thought of creating a separate NSManagedObjectContext to hold these temporary data.
I have a separate class called DatabaseManager to handle core data related tasks. This class initializes with an instance of `NSManagedObjectContext. It creates the managed object classes in the given context.
import CoreData
import Foundation
import MagicalRecord
import SwiftyJSON
public class DatabaseManager {
private let context: NSManagedObjectContext!
init(context: NSManagedObjectContext) {
self.context = context
}
public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
let json = JSON(data)
if let records = json.array {
for recordObj in records {
let record = Record.MR_createInContext(context) as Record
record.id = recordObj["Id"].int
record.name = recordObj["Name"].string!
record.date = NSDate(string: recordObj["Date"].string!)
}
context.MR_saveToPersistentStoreAndWait()
success()
}
}
}
So in the Today view I pass NSManagedObjectContext.MR_defaultContext() to insertRecords() method. I also have a method to fetch Records from the given context.
func fetchRecords(context: NSManagedObjectContext) -> [Record]? {
return Record.MR_findAllSortedBy("name", ascending: true, inContext: context) as? [Record]
}
The data is retrieved from the API, saved in core data and gets displayed successfully. All good so far.
In the Past View, I have to do basically the same thing. But since I don't want the original data to change. I tried to do this a few ways which MagicalRecord provides.
Attempt #1 - NSManagedObjectContext.MR_context()
I create a new context with NSManagedObjectContext.MR_context(). I change the date in Past view, the data for that selected date gets retrieved and saved in the database successfully. But here's the issue. When I fetch the objects from core data, I get that old data as well. For example, each day has only 10 records. In Today view I display 10 records. When the fetch objects in the Past view, I get 20 objects! I assume it's the old 10 objects plus the new ones. Also when I try to display them in the tableview, it crashes with a EXC_BAD_ACCESS error in the cellForRowAtIndexPath method.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let record = records[indexPath.row]
cell.textLabel?.text = record.name // EXC_BAD_ACCESS
cell.detailTextLabel?.text = record.date.toString()
return cell
}
Attempt #2 - NSManagedObjectContext.MR_newMainQueueContext()
The app crashes when I change the date with the following error.
'+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Record''
Attempt #3 - NSManagedObjectContext.MR_contextWithParent(NSManagedObjectContext.MR_defaultContext())
Same result as Attempt #1.
Attempt #4 - From Hal's Answer I learned that even though I create two MOCs, they both refer to the same NSPersistentStore. So I created another new store to hold the temporary data in my AppDelegate.
MagicalRecord.setupCoreDataStackWithStoreNamed("Records")
MagicalRecord.setupCoreDataStackWithStoreNamed("Records-Temp")
Then when I change the date to get the new data, I set that temporary store as the default store like this.
func getDate(date: NSDate) {
let url = NSPersistentStore.MR_urlForStoreName("Records-Temp")
let store = NSPersistentStore(persistentStoreCoordinator: NSPersistentStoreCoordinator.MR_defaultStoreCoordinator(), configurationName: nil, URL: url, options: nil)
NSPersistentStore.MR_setDefaultPersistentStore(store)
let context = NSManagedObjectContext.MR_defaultContext()
viewModel.populateDatabase(date, context: context)
}
Note that I'm using the default context. I get the data but it's the same result as Attempt 1 and 3. I get 20 records. They include data from both the old date and the new date. If I use NSManagedObjectContext.MR_context(), it would simply crash like in Attempt 1.
I also discovered something else. After creating the stores in App Delegate, I printed out the default store name println(MagicalRecord.defaultStoreName()) in the Today's view. Strangely it didn't print the name I gave the store which is Records. Instead it showed Reports.sqlite. Reports being the project's name. Weird.
Why do I get the old data as well? Am I doing something with when initializing a new context?
Sorry if my question is a little confusing so I uploaded a demo project to my Dropbox. Hopefully that will help.
Any help is appreciated.
Thank you.
Thread Safety
First of all I want to mention the Golden Rule of Core Data. NSManagedObject's are not thread safe, hence, "Thou shalt not cross the streams" (WWDC). What this means is that you should always access a Managed Object in its context and never pass it outside of its context. This is why your importer class worries me, you are inserting a bunch of objects into a context without guaranteeing that you are running the insert inside the Context.
One simple code change would fix this:
public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
let json = JSON(data)
context.performBlock { () -> Void in
//now we are thread safe :)
if let records = json.array {
for recordObj in records {
let record = Record.MR_createInContext(context) as Record
record.id = recordObj["Id"].int
record.name = recordObj["Name"].string!
record.date = NSDate(string: recordObj["Date"].string!)
}
context.MR_saveToPersistentStoreAndWait()
success()
}
}
}
The only time you don't need to worry about this is when you are using the Main Queue Context and accessing objects on the main thread, like in tableview's etc.
Don't forget that MagicalRecord also has convenient save utilities that create context's ripe for saving :
MagicalRecord.saveWithBlock { (context) -> Void in
//save me baby
}
Displaying Old Records
Now to your problem, the following paragraph in your post concerns me:
The user can tap on Past button to go to a separate view where he can
choose a past or future date from a date picker view and view Records
for that selected date. This means I have to call the API again
passing the selected date, retrieve the data and save that data in
core data and display them. When the user leaves this view, this data
should be discarded.
I don't like the idea that you are discarding the information the user has requested once they leave that view. As a user I would expect to be able to navigate back to the old list and see the results I just queried without another unecessary network request. It might make more sense to maybe have a deletion utility that prunes your old objects on startup rather than while the user is accessing them.
Anyways, I cannot illustrate how important it is that you familiarize yourself with NSFetchedResultsController
This class is intended to efficiently manage the results returned from
a Core Data fetch request.
You configure an instance of this class using a fetch request that
specifies the entity, optionally a filter predicate, and an array
containing at least one sort ordering. When you execute the fetch, the
instance efficiently collects information about the results without
the need to bring all the result objects into memory at the same time.
As you access the results, objects are automatically faulted into
memory in batches to match likely access patterns, and objects from
previous accessed disposed of. This behavior further serves to keep
memory requirements low, so even if you traverse a collection
containing tens of thousands of objects, you should never have more
than tens of them in memory at the same time.
Taken from Apple
It literally does everything for you and should be your go-to for any list that shows objects from Core Data.
When I fetch the objects from core data, I get that old data as well
Thats to be expected, you haven't specified anywhere that your fetch should include the reports in a certain date range. Here's a sample fetch:
let fetch = Record.MR_createFetchRequest()
let maxDateForThisController = NSDate()//get your date
fetch.predicate = NSPredicate(format: "date < %#", argumentArray: [maxDateForThisController])
fetch.fetchBatchSize = 10// or an arbitrary number
let dateSortDescriptor = NSSortDescriptor(key: "date", ascending: false)
let nameSortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetch.sortDescriptors = [dateSortDescriptor,nameSortDescriptor]//the order in which they are placed in the array matters
let controller = NSFetchedResultsController(fetchRequest: fetch,
managedObjectContext: NSManagedObjectContext.MR_defaultContext(),
sectionNameKeyPath: nil, cacheName: nil)
Importing Discardable Records
Finally, you say that you want to see old reports and use a separate context that won't save to the persistent store. Thats also simple, your importer takes a context so all you would need to do is make sure that your importer can support imports without saving to the persistent store. That way you can discard the context and the objects will go with it. So your method signature could look like this:
public func insertRecords(data: AnyObject, canSaveToPersistentStore: Bool = true,success: () -> Void, failure: (error: NSError?) -> Void) {
/**
Import some stuff
*/
if canSaveToPersistentStore {
context.MR_saveToPersistentStoreWithCompletion({ (complete, error) -> Void in
if complete {
success()
} else {
error
}
})
} else {
success()
}
}
The old data that was in your persistent store, and addressed with the original MOC, is still there, and will be retrieved when the second MOC does a fetch. They're both looking at the same persistent store. It's just that the second MOC also has new data fetched from your API.
A synchronous network operation saving to Core Data will hang your app, and (for a large enough set of records) cause the system to kill your app, appearing to the user as a crash. Your client is wrong on that point, and needs to be educated.
Break apart your logic for fetching, saving, and viewing. Your view that shows a particular date's records should just do that--which it can do, if it accepts a date and uses a predicate.
Your 'cellForRowAtIndexPath' crash smells like a problem with a missing or misspelled identifier. What happens if you hard code a string instead of using 'record.name'?

Resources