NSManagedObjectContext deallocation issues - (Swift | Associated Objects) - swift

I am hoping someone can explain why associated objects in the following example do not get automatically deallocated when the source/host object is deallocated. This example code below is somewhat contrived (apologies in advance), but it explains the my issue.
The example assumes a CoreData entity Product with an string attribute sku and the default CoreData stack provided by the Xcode template:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBAction func createProduct(sender: AnyObject) {
let context = CoreDataHelpers.vendBackgroundWorkerContext()
let newProduct = CoreDataHelpers.newProduct(context: context)
newProduct.sku = "8-084220001"
do {
try newProduct.managedObjectContext?.save()
print("Product created [SKU: \(newProduct.sku ?? "NotDefined")]")
} catch {
print(error)
}
}
}
public class CoreDataHelpers {
public static let mainContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
public class func vendBackgroundWorkerContext() -> NSManagedObjectContext {
let managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
managedObjectContext.parentContext = self.mainContext
return managedObjectContext
}
class func newProduct(context context: NSManagedObjectContext) -> Product {
let newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: context) as! Product
return newProduct
}
}
when the createProduct function is executed, a new PrivateQueueConcurrencyType Managed Object Context (MOC) will be vended and used by the new Product Managed Object (MO). This above code works correctly - so far.
However! if I combine the first two lines of the createProduct function such that:
let newProduct = CoreDataHelpers.newProduct(context: CoreDataHelpers.vendBackgroundWorkerContext())
then the app will crash at try newProduct.managedObjectContext?.save() with a EXC_BAD_ACCESS.
At first glance, this appears a little strange - as all we have done is refactored the code. Digging into the documentation, the managedObjectContext property is declared as unowned(unsafe). This probably means that the created MOC has been deallocated and we have a dangling pointer (please correct me if my assumption is wrong).
In order to ensure that MOC does not get deallocated, I tried associating it with the MO itself. newProduct:
class func newProduct(context context: NSManagedObjectContext) -> Product {
let newProduct = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: context) as! Product
var key: UInt8 = 0
objc_setAssociatedObject(newProduct, &key, context, .OBJC_ASSOCIATION_RETAIN)
return newProduct
}
This appears to works wonderfully - until I check in Instruments. It would appear that when the Product MO is deallocated, the now associated MOC is not (shouldn't it be automatically deallocated when the source object is deallocated?)
My question is:
Can someone explain where the additional reference is to the MOC that is preventing it from being deallocated? Have I created a retain cycle between the MO and the MOC?

You are probably creating a circular ownership (retain cycle).
Every managed object is owned by a managed context (the context owns the object) and setting the context as associated object means that the object now also owns the context.
Therefore, they won't get deallocated.
The real solution is to save the background context to a local property, the same you are doing with mainContext.

Related

Simple macOs app gets EXC_BAD_ACCESS for CoreData accessing one to many object

I have very simple app, that has a CoreData database and in it, it has two type of objects, one that is the main object, and the other. The main object, Object A, can have many Object B. But Object B, can be connected to only one Object A.
My problem is, after a while of the app running, it runs into EXC_BAD_ACCESS error.
To be precise:
Thread 85: EXC_BAD_ACCESS (code=1, address=0x77e341213c20)
I have done some debugging, and it looks like this only happens, when I open the SwiftUI part of the interface, and possibly make changes to the db. I have read in forums, that it's a Thread issue and access. I tried database setup mentioed there (I am copying here) but that still runs into the error.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
return result
}()
var context: NSManagedObjectContext {
return container.viewContext
}
var container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp")
// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.newBackgroundContext()
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: {(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
My question is, how can I debug this? I understand the fact, this is trying to access the array of Objects B, that is already released, I just don't understand why it's released. Could it be because I opened the SwiftUi window and closed it? But why doesn't the connection just keep it?
Is there a way to prevent this error? I can see 3 threds running in the debugger, when the excecption is thrown, but I'm not aware of "creating new thread" and being new to Swift, not sure how to start a one, or stop one from being created.
Apart from passing the context down directly to the view, in two places I use a helper that looks like this:
public func getManagedContext() -> NSManagedObjectContext {
return (NSApplication.shared.delegate as! AppDelegate).coreDataStack.context
}
You can try fetching values in DipatchQueue.main block
it will avoid blocking the current thread.

Saving segue passed managedObjectContext in Swift

say i have one view controller (VCA), which segues to another another view controller (VCB). while preparing to segue, VCA passes its managedObjectContext (i.e. following the "tell don't ask" convention). VCA also has a function saveManagedObjectContext() that performs the saving and error handling.
so, in VCA:
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
...
func saveManagedObjectContext() {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
abort()
}
}
}
...
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
if let vcb = segue.destinationViewController as? VCB {
vcb.managedObjectContext = managedObjectContext
}
...
}
and in VCB:
var managedObjectContext: NSManagedObjectContext? = nil
say VCB adds some entity to the managedObjectContext that was passed to it from VCA
func createSomeEntity() {
let entity = NSEntityDescription.entityForName("SomeEntity", inManagedObjectContext: managedObjectContext!)
something = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedObjectContext) as? SomeEntity
}
is there any (correct) way to call the saveManagedObjectContext() function from VCA rather than having to copy the function over to VCB?
thanks
An alternative is a singleton class.
Create a new Swift file, replace the predefined code with
class CoreDataManager: NSObject {
// MARK: - Shared Instance
class var sharedManager : CoreDataManager {
struct Singleton {
static let instance = CoreDataManager()
}
return Singleton.instance
}
// MARK: - Core Data stack
lazy var ...
}
and then replace lazy var ... with the entire Core Data stack from AppDelegate.
Now you can access Core Data from everywhere using
let managedObjectContext = CoreDataManager.sharedManager.managedObjectContext
or to call the save action
CoreDataManager.sharedManager.saveAction(self)
Good point about "tell don't ask". A lot of Apple sample code evangelizes this concept. However, in recent years, Apple has also provided sample code with a Core Data stack class that handles the object graph, practically abandoning the "tell don't ask" pattern.
Also, in many popular and acclaimed open source projects, this pattern is used. In most cases you reduce the code and still have a robust solution. For example, in more complex projects with nested background contexts it is often the only feasible setup.
Thus, I would recommend to create a CoreDataManager class that handles the core data stack, or for less complex apps (single, main thread context) use the app delegate.
Note that in Swift, you can really make this very concise with global variables (which you should use sparingly!). E.g., on top of the AppDelegate.swift you could write
let SharedAppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
(I like to capitalize my global singletons). You can use this as follows
do { try SharedAppDelegate.context.save() } catch {}

How to use the delegates with NSKeyedUnarchiver?

I am using NSKeyedUnarchiver to unarchive an object and would like to use the delegates (NSKeyedUnarchiverDelegate), but my delegates are not called. Archiving and Unarchiving is working fine, but the Delegates (unarchiver & unarchiverDidFinish) are not called. Can someone help?
I have the following implementation:
class BlobHandler: NSObject , NSKeyedUnarchiverDelegate{
func load() -> MYOBJECTCLASS{
let data:NSData? = getBlob();
var mykeyedunarchiver:NSKeyedUnarchiver=NSKeyedUnarchiver(forReadingWithData: data!);
mykeyedunarchiver.delegate = self;
let temp=mykeyedunarchiver.decodeObjectForKey("rootobject")
// No delegates are called
if temp==nil {
blobsexists=false;
}else{
objectreturn = temp! as! MYOBJECTCLASS;
return objectreturn;
}
}
func save1(myobject:MYOBJECTCLASS){
let data = NSMutableData()
var keyedarchiver:NSKeyedArchiver=NSKeyedArchiver(forWritingWithMutableData: data);
keyedarchiver.encodeObject(maptheme, forKey: "rootobject");
let bytes = data.bytes;
let len=data.length;
saveblob(bytes);
}
The following delegates, which are also implemented in my Blobhandler, are never called:
func unarchiver(unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? {
print("I am in unarchiver !");
return nil;
}
func unarchiverDidFinish(_ unarchiver: NSKeyedUnarchiver){
print("I am in unarchiverDidFinish ! ");
}
I don't know what it was, but its working after a clean and rebuild of the project.
I notice with different cases, that the builds are not in sync sometimes. There is sometimes code, which is in XCode but it is not executed. Sounds unbelievable, but I guess its true.
XCode 7.2
I think the first function is never called since you didn't actually feed a "cannotDecodeObjectOfClassName" at all, since you only did try to unarchive previously archived data. You can try this method(or something requires a class name) to validate your solution(feed a class doesn't conform NSCoding):
unarchiver.decodeObjectOfClass(cls: NSCoding.Protocol, forKey: String)
The second one is a little bit tricky. I've tried this method in a similar situation and it turned out that unarchiverDidFinish only get called when a complete unarchiving job is done and probably before it's destroyed. For example, I had a NSCoding class and the convenience initiator is like
required convenience init?(coder aDecoder: NSCoder) {
let unarchiver = aDecoder as! NSKeyedUnarchiver
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
unarchiver.delegate = appDelegate.uad
let name = unarchiver.decodeObjectForKey(PropertyKey.nameKey) as! String
print(321)
self.init(name: name, photo: photo, rating: rating)
}
uad is an instance of class:
class UAD:NSObject, NSKeyedUnarchiverDelegate {
func unarchiverDidFinish(unarchiver: NSKeyedUnarchiver) {
print(123)
}
}
And in the view controller the loading process is like
func load() -> [User]? {
print(1)
let ret = NSKeyedUnarchiver.unarchiveObjectWithFile(ArchiveURL.path!) as? [User]
print(2)
return ret
}
And the output is like:
1
321
321
321
321
321
123
2
After finishing loading a group of users, the unarchiverDidFinish finally got called once. Notice that this is a class function and an anonymous instance is created to finish this sentence:
NSKeyedUnarchiver.unarchiveObjectWithFile(ArchiveURL.path!) as? [User]
So I really believe that this function only get called before it is destroyed or a group of call back functions is finished.
I am not quite sure if this is the case for you. You may try to make your unarchiver object global and destroy it after your loading is done to see whether this function is called.
Correct me if anything not right.
To make either unarchiverWillFinish: and unarchiverDidFinish: be called properly, we have to invoke finishDecoding when finished decoding.
Once you have the configured decoder object, to decode an object or data item, use the decodeObjectForKey: method. When finished decoding a keyed archive, you should invoke finishDecoding before releasing the unarchiver.
We notify the delegate of the instance of NSKeyedUnarchiver and perform any final operations on the archive through invoking this method. And once this method is invoked, according to Apple's official documentation, our unarchiver cannot decode any further values. We would get following message if we continue to perform any decoding operation after invoked finishDecoding:
*** -[NSKeyedUnarchiver decodeObjectForKey:]: unarchive already finished, cannot decode anything more
It also makes sense for encoding counterparts.

Communicating with AppDelegate across scenes (Cocoa, swift)

In Xcode 6 (7 too), I'm looking to find out how to get managedObjectContext and use it in a ViewController in a different scene.
I started a new Cocoa project, clicked 'Use CoreData'
Added an entity in CoreData, generated a managedObject for it
Added an array controller, and bound it to the entity
I want to bind the array controller to the managedObjectContext, but I also want to bind the tableView (in my view controller) to the array controller. They are in different scenes, which seems to mean different namespaces.
What I've tried to do:
Set an instance variable in ViewController:
var managedObjectContext: NSManagedObjectContext!
In viewDidAppear() I added:
if let app = NSApplication.sharedApplication().delegate! as? AppDelegate {
if let context = app.managedObjectContext{
managedObjectContext = context
} else {
print("There was no context available, didn't work")
}
}
Then I bound the columns of the table to the properties of the entity. And cocoa bindings autocompleted, meaning the context was at least recognized properly.
However when I run it, it fails silently with: 'Cannot perform operation without a managed object context'. When debugging the context is being set as a real object, but I have no idea if it's actually initialized. I looked through the docs and cocoa binding troubleshooting but this seems to be a coredata issue.
(I've looked here: Getting managedObjectContext from AppDelegate but I can't override the normal init in swift)
I made one example for you. I have one Entity, called Person with 2 attributes, name and age. This is my ViewController:
import Cocoa
class ViewController: NSViewController {
// ManagedObjectContext from AppDelegate
lazy var moc: NSManagedObjectContext = {
let appDel = NSApplication.sharedApplication().delegate as! AppDelegate
return appDel.managedObjectContext
}()
override func viewDidLoad() {
super.viewDidLoad()
// Populate some sample data
let firstPerson = NSEntityDescription.insertNewObjectForEntityForName("Person", inManagedObjectContext: moc) as! Person
firstPerson.name = "Jesse Pinkman"
firstPerson.age = 25
}
}
And in IB i have one table view and one array controller. Set array controller's entity to Person:
And the bound your array controller's managed object context to your viewcontroller's managed object context, in my example self.moc:
And then just bound your table view's colum's to your array controller.
For what it's worth, you can bind to the Application in IB, with a model key path of self.delegate.managedObjectContext. This is the quick and dirty way.
Some people argue that this is bad, mostly because they think the app delegate shouldn't have the MOC in the first place. I think the app delegate owning the core data stack is fine.
But I'd warn it's not future-proof. If you want to do something like create a child MOC for the VC, and have an easy "undo" of all the local changes in that scene by skipping the save, you'd end up creating and maintaining the moc property on the VC.

Core Data stack implementation for iCloud sync in a UITabBarController app (Swift 1.2)

I've spent the last 4 days trying to implement a proper Core Data stack with iCloud sync for my Swift 1.2 app, but I can really use some help.
Before, I was using a global Managed Context accessed from everywhere in the app; knowing that it was a bad implementation, now that I'm adding iCloud sync I decided to get rid of it, even though the app was working fine.
So far, I've implemented a new, working Core Data stack with decent - but not perfect - cloud sync between devices.
Now I face two issues:
Sometimes, a few objects don't sync.
Given the particular structure of my app, which I'll explain in a moment, I have no idea how and where in my code I should handle the notifications that Core Data sends when the user logs in or out of iCloud.
But, before tackling those problems, I'd really appreciate - if appropriate - some validation of the work I've done so far and it is mainly for some confirmations that I'm writing this: since I've already spent a lot of time changing my Core Data stack, before going forward I'd like to know if I'm propagating the context properly (the structure of my app doesn't conform to any tutorial I found online, so I had to improvise a bit), or if I made some basic mistakes that will compromise reliable syncing or the future development.
My app is structured as follow:
UITabBarViewController as initial ViewController
1st tab: UIViewController (shown when the app starts)
2nd tab: a UITableViewController embedded in a UINavigationController
3rd tab: another UITableViewController embedded in another UINavigationController
I have a CoreDataStack.swift class with the following code:
import CoreData
#objc class CoreDataStack : Printable {
let context : NSManagedObjectContext
let psc : NSPersistentStoreCoordinator
let model : NSManagedObjectModel
let store : NSPersistentStore?
var description : String {
return "context: \(context)\n" + "model: \(model)"
}
var applicationDocumentsDirectory : NSURL = {
let fileManager = NSFileManager.defaultManager()
let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) as! [NSURL]
return urls[0]
}()
init() {
let modelURL = NSBundle.mainBundle().URLForResource("MyDataModel", withExtension:"momd")
model = NSManagedObjectModel(contentsOfURL: modelURL!)!
psc = NSPersistentStoreCoordinator(managedObjectModel: model)
context = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType)
context.persistentStoreCoordinator = psc
let documentsURL = applicationDocumentsDirectory
let storeURL = documentsURL.URLByAppendingPathComponent("MyApp.sqlite")
let options = [NSPersistentStoreUbiquitousContentNameKey: "MyApp", NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
store = psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options, error:&error)
if store == nil {
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict as [NSObject : AnyObject])
println("Error adding persistent store: \(error), \(error!.userInfo)")
abort()
}
}
func saveContext() {
var error: NSError? = nil
if context.hasChanges && !context.save(&error) {
println("Could not save: \(error), \(error!.userInfo)")
}
}
var updateContextWithUbiquitousContentUpdates: Bool = false {
willSet {
ubiquitousChangesObserver = newValue ? NSNotificationCenter.defaultCenter() : nil
}
}
private var ubiquitousChangesObserver : NSNotificationCenter? {
didSet {
oldValue?.removeObserver(self, name: NSPersistentStoreDidImportUbiquitousContentChangesNotification, object: psc)
ubiquitousChangesObserver?.addObserver(self, selector: "persistentStoreDidImportUbiquitousContentChanges:", name: NSPersistentStoreDidImportUbiquitousContentChangesNotification, object: psc)
}
}
func persistentStoreDidImportUbiquitousContentChanges(notification: NSNotification) {
println("Merging ubiquitous content changes")
context.performBlock {
self.context.mergeChangesFromContextDidSaveNotification(notification)
}
}
}
In my AppDelegate.swift I added the following code just under var window: UIWindow?:
lazy var coreDataStack = CoreDataStack()
coreDataStack.updateContextWithUbiquitousContentUpdates = true
// The following code is the way I found to propagate the managed context of the stack instantiated above in all the ViewControllers of the UITabBarController, including those embedded in the two NavigationControllers;
// since in the future I'll probably need some flexibility in term of adding / rearranging the VCs in the TabBar, I kind of like this way to pass around the context.
// I could have also passed the context to the CustomTabBarViewController and from there do the same thing, but I figured I could just pass the context from AppDelegate, since I already can access all the ViewControllers from here with the following code.
var tabBarController = self.window!.rootViewController as! CustomTabBarViewController
for eachViewController in tabBarController.viewControllers! {
if eachViewController.isKindOfClass(CustomViewController){
(eachViewController as! CustomViewController).passedManagedContext = coreDataStack.context // Context is passed to the VC of 1st tab
}
if eachViewController.isKindOfClass(UINavigationController){
var firstNavController = tabBarController.viewControllers![1] as! UINavigationController
for tvc in firstNavController.viewControllers! {
if tvc.isKindOfClass(FirstCustomTableViewController) {
(tvc as! FirstCustomTableViewController).passedManagedContext = coreDataStack.context // Context is passed to the TableVC inside the NavigationController in tab 2
}
}
var secondNavController = tabBarController.viewControllers![2] as! UINavigationController
for tvc in secondNavController.viewControllers! {
if tvc.isKindOfClass(SecondCustomTableViewController) {
(tvc as! SecondCustomTableViewController).passedManagedContext = coreDataStack.context // Context is passed to the TableVC inside the NavigationController in tab 3
}
}
}
}
// Of course, in applicationDidEnterBackground: and applicationWillTerminate: I save the context; obviously, I also save the context, when appropriate, from the other ViewControllers.
With this structure in place, I instantiate my stack in AppDelegate and from there I propagate it to the 3 elements of the TabBar; from those, I again propagate the context to every other ViewController I present. I logged to the console the context everywhere and I can confirm that it is always the same.
As a matter of fact, the app with this code works.
I can't say it is perfect because, as I said, sometimes a few objects don't sync, but I suspect the cause of those objects not syncing is another (briefly, I have 2 NSManagedObject subclasses; the objects of subclass1 have an object of subclass2 as property; if I create a new subclass1 object using an existing subclass2 object as property, sync is fine; if I also create a new subclass2 object, save it and immediately set it as property of subclass1, sometimes the subclass2 object doesn't sync on the other device, while the subclass1 does and then misses that property... I can work on that later).
Before digging into this sync issue, I'd really love to know if the work I've done so far with the stack makes sense, or if it is horrible and needs to be canned.
Then, if all the code above is not horrible and if the reason of the occasional missed sync of objects would turn out to be the one I suspect, comes the other issue, and it is a big one: where do I put the code to handle the notifications that occurr when the user logs in or out from iCloud (NSPersistentStoreCoordinatorStoresWillChangeNotification and NSPersistentStoreCoordinatorStoresDidChangeNotification)?
I tried to put methods I've written (without actual functionality, at the moment I only log something to the console to know that I got there) based on Core Data by Tutorials book in both my AppDelegate and my CoreDataStack class, but in both cases when I log in or out from iCloud while the app is running, the app crashes without a single line in the console, so I have no idea of the issue.
Maybe I should put the methods to handle these notifications in all the ViewControllers, since the fetch requests happen there and UI is updated from those classes, but I'm not passing the entire coreDataStack objects around, only the context... so I'm missing something. Should I pass the entire stack, not only the context? Is it okay to handle those notifications from my CoreDataStack, or should I do it from AppDelegate?
Any help would really be appreciated...
Thanks in advance and, please, excuse if my question is not clear (I'm quite a beginner and english is not my main language...).
Also, thank you for your time reading this long question!
#cdf1982
I think the problem is that iCloud + CD never worked properly. It's not a developer code issue, the problem is the Apple implementation of iCloud + CD that simply fails.