Shared Instance of NSManagedObject

I have come upon a situation, in the project that I’m working on, where I need a single shared instance of NSManagedObject. I looked around for a common practice and there was none except for some misguiding Q&As on Stack Overflow. There were (are) probably more people than I that need this kind of functionality yet they didn’t publish their solutions. That or I didn’t try hard enough to actually find them. So I decided to whip it up.

I decided to whip up my own solution not only because I was struggling to find an existing one, but also because it was something new and something that I haven’t stumbled upon before.

The setup: let’s say that I have a fictional project which has users and something else that users can add, edit, delete and read. In the app itself I’d like to have a very quick access to default user account. For the sake of clarity: WSAccount entity represents the account entity and it is being wrapped by WSAccount class instances in the code. Since it is supposed to be a shared instance, I started smelling singleton pattern. I do know that singletons are evil. To be honest - I really do my best to actually avoid singletons in my code as hard as I can. I think for the first time in my coding career I was saved by constraints put on framework by its creators. And here’s why.

Since NSManagedObject depends on NSManagedObjectContext. And NSManagedObjectContext is not thread safe, I figured that shared instance should be reinstantiated depending in which context you require it to be. Hence the method name was born:

1
2
3
4
5
@interface WSAccount : NSManagedObject

+ (id)sharedInstanceInContext:(NSManagedObjectContext:)managedObjectContext;

@end

It may be weird for you, but I am one of those guys that don’t mind passing existing or new managed object context from one view controller to another. For me fetching the default managed object context from app delegate all the time is really uncomfortable. Including that “AppDelegate.h” in every possible file (it’s just one of those ways when headers cripple your code), doing the casting. Ugh…

For me, passing the managed object context along the application flow seems more… flowy. It also benefits you from actual possibility to have transient managed objects, not worry about failed validations and existing objects (fetched in other MOCs) can be retrieved in a single call!

Now tagging along the implementation, I defined the static variable and assigned it to nil (just like in singleton!!!).

1
2
3
4
5
@implementation WSAccount

static WSAccount *_ws_sharedAccountInstance = nil;

...

Now for the actual method implementation - it’s quite simple. There is one extra check to be made before doing the fetching thing. Here’s the code with comments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
+ (id)sharedAccountInContext:(NSManagedObjectContext *)context {
      // Do this without allowing anyone access to shared instance variable
    @synchronized(_ws_sharedAccountInstance) {
          // If the account instance is missing
        if (!_ws_sharedAccountInstance) {
              // perform the fetch!
            NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
            NSEntityDescription *entity = [NSEntityDescription entityForName:@"WSAccount" inManagedObjectContext:context];
            [fetchRequest setEntity:entity];
            [fetchRequest setFetchLimit:1];

            // if you want to restrict the search, this 
            // would be the place to add fetch predicates
            // NSPredicate *fetchPredicate = [NSPredicate predicateWithFormat:<#format...#>];
            // [fetchRequest setPredicate:fetchPredicate];

            NSError *fetchError = nil;
            NSArray *results = [context executeFetchRequest:fetchRequest error:&fetchError];
            if (!fetchError && [results count]) {
                  // assign the first object of result array to the variable
                _ws_sharedAccountInstance = [results lastObject];
            } // otherwise leave the variable set to nil
        } else if (![[_ws_sharedAccountInstance managedObjectContext] isEqual:context]) {
              // now if we are requesting same object in different context
              // retrieve it via instance of managed object id
            NSManagedObjectID *moID = [_ws_sharedAccountInstance objectID];
            _ws_sharedAccountInstance = nil;

            NSError *existenceError = nil;
            VCAccount *account = (VCAccount *)[context existingObjectWithID:moID error:&existenceError];
            if (!existenceError && account) {
                  // assign retrieved object
                _ws_sharedAccountInstance = account;
            }
            // otherwise leave it at nil to indicate that
            // no such instance in this context exists
        }
    }

    // afterwards return fetched instance or nil
    return _ws_sharedAccountInstance;
}

Comments