├── .gitignore ├── README.md └── source ├── CoreDataStack.h └── CoreDataStack.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2016 onwards 2 | ============ 3 | 4 | NOTE: Apple's new NSPersistentContainer has been created to serve most of the purpose that CoreDataStack fulfilled; I strongly advise you try using the Apple class from here on instead of this one. 5 | 6 | https://developer.apple.com/library/content/releasenotes/General/WhatNewCoreData2016/ReleaseNotes.html 7 | 8 | 9 | CoreDataStack 10 | ============= 11 | 12 | Simple classes to make Apple's Core Data usable for normal human beings... 13 | 14 | ...WITHOUT CHANGING the way CoreData works (i.e. this is not a "framework" - it's just a very thin wrapper to do the code that Xcode "New Project" template could have done in the first place. 15 | 16 | Core Data is very easy to use - but Apple's source examples make it harder than it should be. 17 | 18 | We fix: Xcode's bad examples / bad practices 19 | ===== 20 | 21 | 1. Xcode's default template for "a CoreData enabled app" puts 300 lines of INCORRECT, UNNECESSARY code into your Project. 22 | 2. Xcode puts it all into AppDelegate - the worst possible place for this code. 23 | 3. Apple's sample code forces you to have ONLY ONE CORE DATA MODEL open at a time. This is wrong, and throws away one of CoreData's awesome features: most CoreData projects would be many times easier to write and debug if they used multiple models. They would also run faster. But if you start with Apple's template, you cannot do this. CoreData was not designed to be used that way! 24 | 25 | So, this simple library turns CoreData from: 26 | 27 | "300 lines of dense code in your AppDelegate class" 28 | 29 | ...into: 30 | 31 | "3 lines of simple, easy code you can re-use throughout your project" 32 | 33 | License is: 34 | 35 | Do whatever you want with this code. Attribution is appreciated - @t_machine_org on twitter - but not required. 36 | 37 | 38 | Installation A: by drag/drop into your project 39 | ===== 40 | 41 | 1. Open the "source" folder 42 | 2. Drag/drop all the files into your project 43 | 44 | 45 | Installation B: by creating a static library 46 | ===== 47 | 48 | Not supported yet. Use "Installation A" instead (see above) If you really want it, add an item to the Issue Tracker on Github, and I'll make a library version. 49 | 50 | 51 | Usage 52 | ===== 53 | 54 | To use CoreData, you ALWAYS need to have created a CoreData Model first. You also ALWAYS need to have defined a save location for the database. 55 | 56 | Apple makes this difficult, but we make it easy again. All you need to know is the NAME of your CoreData Model (the graphical doc that lets you edit and add entities) - whatever name it appears in on the left hand side of Xcode. e.g. if it says "CoreDataModel.xcdatamodeld" then the "NAME" is "CoreDataModel" 57 | 58 | Given the name, you do: 59 | 60 | CoreDataStack* stack = [CoreDataStack coreDataStackWithModelName:@"My_Model_Name"]]; 61 | 62 | Usage - Modern, improved versions of Apple methods 63 | ----- 64 | 65 | Most of the "common" actions you need to do with CoreData have strange method names, or exist on a different class from the one you were expecting. 66 | 67 | Many of them take NSString arguments where a Class would be easier and more appropriate (NB: XCode autocomplete does NOT support Apple's strings - it only 68 | supports classes!) 69 | 70 | So ... this also includes a bunch of methods that fix those two problems. Using them is entirely optional - they merely reduce the boilerplate code 71 | in your project, and make it typesafe. 72 | 73 | e.g.: 74 | 75 | - insertInstanceOfClass: (creates a new NSManagedObject of the specified class) 76 | - fetchEntities:matchingPredicate: (gets all entities of the specified class that match a predicate) 77 | - countEntities:matchingPredicate: (counts the number of possible results) 78 | - storeContainsAtLeastOneEntityOfClass: (is this the first run of your app? Did the user wipe the CoreData store? Check this when starting your app...) 79 | - ...etc 80 | 81 | Usage - Fallback to Apple basics 82 | ----- 83 | 84 | Then, everytime CoreData needs a "ManagedObjectContext" instance, you just pass it: 85 | 86 | stack.managedObjectContext 87 | 88 | e.g.: 89 | 90 | NSEntityDescription* entityDesc = [NSEntityDescription entityForName:@"MyFirstEntity" inManagedObjectContext:stack.managedObjectContext]; 91 | 92 | ...or, better, because the line above is BAD PRACTICE (even though Apple uses it in their source examples), use: 93 | 94 | NSEntityDescription* entityDesc = [stack entityForClass:[MyFirstEntity class]]; 95 | 96 | NB: if you use Apple's version, which takes an NSString, then refactoring in Xcode *will NOT work!* and any small typo will NOT be detected by the compiler - your app will instead crash at runtime. So ... don't pass in an NSString, pass in the class you want instantiated. 97 | 98 | 99 | Easy! 100 | -------------------------------------------------------------------------------- /source/CoreDataStack.h: -------------------------------------------------------------------------------- 1 | /** 2 | CoreDataStack - CoreData made easy 3 | 4 | c.f. https://github.com/adamgit/CoreDataStack for docs + support 5 | 6 | The most important method here is: 7 | 8 | coreDataStackWithModelName: 9 | 10 | (this creates a new instance, pointing to the CoreData MOM/MOMD file that was generated by Xcode) 11 | 12 | --------------- Important note --------------- 13 | 14 | NB: Apple in many places uses an NSString argument; you should never use a string argument! That makes unsafe code, and it reveals a major bug in Apple's 15 | Xcode refactoring (Xcode "refactor rename" WILL CORRUPT your project, in all known versions of Xcode, if you use it on a CoreData class). 16 | 17 | Its bad practice. There's good reason for allowing String arguments (it enables CoreData to work with classes that it doesn't actually have the class file) 18 | but that's a very rare case for most projects. I have never seen it on a live project (although I could construct a theoretical project which needed it). 19 | 20 | This class is *compatible* with Apple's approach (you can always access the NSManagedObjectContext directly, and use Apple's NSString-based methods *if you 21 | need to*), but it discourages you from using them. 22 | 23 | As a bonus, when using this class, XCode autocomplete works again (Apple disabled autocomplete for CoreData classes back in XCode 4.0, and still hasn't 24 | added it back again). 25 | */ 26 | #import 27 | 28 | #import 29 | 30 | #define kNotificationDestroyAllNSFetchedResultsControllers @"DestroyAllNSFetchedResultsControllers" 31 | typedef enum CDSStoreType 32 | { 33 | CDSStoreTypeUnknown, 34 | CDSStoreTypeXML, 35 | CDSStoreTypeSQL, 36 | CDSStoreTypeBinary, 37 | CDSStoreTypeInMemory 38 | } CDSStoreType; 39 | 40 | @class CoreDataStack; 41 | 42 | typedef void (^CoreDataStackErrorHandler)(CoreDataStack* stack, NSError* error); 43 | 44 | @interface CoreDataStack : NSObject 45 | { 46 | NSManagedObjectModel* _mom; 47 | NSManagedObjectContext* _moc; 48 | NSPersistentStoreCoordinator* _psc; 49 | } 50 | 51 | /** Apple's CoreData officially does not support Multi-Threading. If you force it to run single-threaded 52 | (which is non-trivial and requires NOT using some of the official multi-threading features of iOS!), 53 | but make a mistake ... instead of failing, it silently corrupts your datastores while running! 54 | 55 | So, until Apple fixes their thread-unsafe code - or introduces proper assertions / error-handling to 56 | guard against it - most projects need an "emergency failsafe" to stop your app from "accidentally" 57 | accessing the thread-unsafe code. 58 | 59 | What we do: 60 | 61 | The first thread that uses a stack is THE ONLY thread we allow to access the stack ("uses" and "access" here mean: "reads the .managedObjectContext 62 | property") 63 | 64 | OFFICIALLY from Apple docs: it is not just "writes" to objects that are dangerous, but also "reads": 65 | 66 | http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/coredata/Articles/cdConcurrency.html#//apple_ref/doc/uid/TP40003385-SW1 67 | 68 | "Core Data does not present a situation where reads are “safe” but changes are “dangerous”—every operation is “dangerous” because every operation has cache coherency effects and can trigger faulting" 69 | */ 70 | @property(nonatomic,assign,readonly) NSThread* threadThatOwnsThisStack; 71 | 72 | /*! The actual URL that's in use - you can pass this to init, or let the CoreDataStack work it out automatically */ 73 | @property(nonatomic,retain) NSURL* databaseURL; 74 | /*! Name of the model file in Xcode, with no extension - e.g. for "Model.xcdatamodeld", this is "Model" */ 75 | @property(nonatomic,retain) NSString* modelName; 76 | /*! Apple crashes if you don't tell it the correct 'type' of CoreData store. This class will try to guess it for you, and seems to get it right - if not, you can explicitly set it during or after init */ 77 | @property(nonatomic) CDSStoreType coreDataStoreType; 78 | /** 79 | Defaults to NSConfinementConcurrencyType (was the only option prior to iOS 5) 80 | 81 | Changing this *after* you've generated a MOC (by calling the .managedObjectContext getter) will cause an asertion - you really shouldn't do that 82 | */ 83 | @property(nonatomic) NSManagedObjectContextConcurrencyType managedObjectContextConcurrencyType; 84 | 85 | /*! Most apps need this set to "TRUE", but Apple defaults to FALSE, so we default to FALSE too */ 86 | @property(nonatomic) BOOL automaticallyMigratePreviousCoreData; 87 | 88 | /** Very useful when developing an app: make "Failed saves" *immediately* Assert (pause the debugger), so you can quickly debug them, 89 | instead of silently failing and you not noticing that your in-memory data is corrupt! */ 90 | @property(nonatomic) BOOL shouldAssertWhenSaveFails; 91 | 92 | /** add a handler block for errors that could happen in persistent store creation or updating. Called whenever no other error handling block is explicitly passed as parameter to any of this classes methods */ 93 | @property(nonatomic) CoreDataStackErrorHandler defaultErrorBlock; 94 | 95 | /*! To use CoreData, you need to provide a permanent filename for it to save its sqlite DB to disk - or provide a manual URL to where 96 | you've already saved it. You also need to provide a model name 97 | 98 | For instance, in Xcode's visual editor for CoreData, the visual file itself is your "model", and the filename is something like: 99 | 100 | "CoreData.mom" 101 | "Model.momd" 102 | 103 | ...or similar. 104 | 105 | Generally, you should ignore Xcode's defaults and rename that to something useful, like: 106 | 107 | "ModelForUploads.mom" 108 | "UserAccountsModel.momd" 109 | 110 | Whatever you call it ... the first part of the filename (everything before .mom / .momd) is what you provide to this method in your source code to 111 | access CoreData. 112 | */ 113 | +(CoreDataStack*) coreDataStackWithModelName:(NSString*) mname; 114 | 115 | /*! 116 | NB: either keep this reference and re-use it throughout your app, or use the other version of this method that returns a "shared" pointer, 117 | otherwise you'll get CoreData inconsistency errors 118 | */ 119 | +(CoreDataStack*) coreDataStackWithModelName:(NSString *)mname databaseFilename:(NSString*) dbname; 120 | 121 | /** 122 | Same as the other coreDataStackWithModelName methods, except this caches the result and if you call it again 123 | later with the same arguments you get a pointer to the same object. 124 | 125 | This is convenient when you don't want to pass a CoreDataStack pointer back-and-forth between all your UIViewControllers, 126 | and makes it act like a singleton 127 | */ 128 | +(CoreDataStack*) coreDataStackWithSharedModelName:(NSString *)mname databaseFilename:(NSString*) dbname; 129 | 130 | /** 131 | Lower-level version of coreDataStackWithModelName that uses the underlying file URL. 132 | 133 | Mostly useful for recreating the exact same stack later on, e.g.: 134 | 135 | firstStack = [CoreDataStack coreDataStackWithModelName: @"MyModel"]; 136 | secondStack = [CoreDataStack coreDataStackWithDatabaseURL: firstStack.databaseURL]; // uses the same config data, but is NOT shared 137 | */ 138 | +(CoreDataStack*) coreDataStackWithDatabaseURL:(NSURL*) dburl modelName:(NSString*) mname; 139 | 140 | /** 141 | Generally, you should NOT use this method; use the static "coreDataStackWith..." methods instead, which 142 | use this method and automatically generate the non-obvious arguments for you 143 | */ 144 | - (id)initWithURL:(NSURL*) url modelName:(NSString *)mname storeType:(CDSStoreType) type; 145 | 146 | /** Apple's NSManagedObjectModel in this stack */ 147 | -(NSManagedObjectModel*) dataModel; 148 | 149 | /** Apple's NSPersistentStoreCoordinator in this stack */ 150 | -(NSPersistentStoreCoordinator*) persistentStoreCoordinator; 151 | 152 | /** Apple's NSManagedObjectContext in this stack 153 | 154 | NB: your app will FREQUENTLY reference this - the managedObjectContext ("MOC") is the main starting point needed 155 | for almost all CoreData actions 156 | */ 157 | -(NSManagedObjectContext*) managedObjectContext; 158 | 159 | #pragma mark - boilerplate methods which you'll need on every project but Apple didn't include 160 | 161 | /*! Useful method that most apps need, to check instantly whether they've been initialized with this CD store before */ 162 | -(BOOL) storeContainsAtLeastOneEntityOfClass:(Class) c; 163 | 164 | /*! Every app MUST save the context sooner or later, but Apple's implementation of CoreData doesn't support Blocks. 165 | 166 | Let's fix that! 167 | 168 | NB: the syntax for Apple's non-blocks-based "save" is very easy to get wrong, and requires you to declar an Error 169 | var every time you use it. This is much simpler. 170 | */ 171 | -(void) saveOrFail:(void(^)(NSError* errorOrNil)) blockFailedToSave; 172 | 173 | /** Shorthand for Apple's clunky method: 174 | 175 | [NSEntityDescription entityForName: inManagedObjectContext:] 176 | 177 | NB: Apple uses an NSString argument; you should never use a string argument! (c.f. top of this file for more details) 178 | 179 | ...instead, call this method for an entity named e.g. Entity (Apple's default entity name): 180 | 181 | "[stack entityForClass:[Entity class]];" 182 | 183 | NB: Obviously, this implementation automatically uses the stack's current NSManagedObjectContext as the final argument - anything else wouldn't make sense 184 | */ 185 | -(NSEntityDescription*) entityForClass:(Class) entityClass; 186 | 187 | /** 188 | Shorthand for Apple's hard-to-remember method that creates new CoreData objects: 189 | 190 | [NSEntityDescription insertNewObjectForEntityForName: inManagedObjectContext:] 191 | 192 | NB: Apple uses an NSString argument; you should never use a string argument! (c.f. top of this file for more details) 193 | 194 | ...instead, call this method for an entity named e.g. Entity (Apple's default entity name): 195 | 196 | "[stack insertInstanceOfClass:[Entity class]];" 197 | 198 | NB: Obviously, this implementation automatically uses the stack's current NSManagedObjectContext as the final argument - anything else wouldn't make sense 199 | */ 200 | -(NSManagedObject*) insertInstanceOfClass:(Class) entityClass; 201 | 202 | /** 203 | Shorthand for Apple's over-complicated, String-based, method that fetches existing CoreData objects: 204 | 205 | [NSFetchRequest fetchRequestWithEntityName:] 206 | 207 | This returns a fetchRequest that you can then configure with a predicate, sortDescriptors, etc as usual (as per Apple docs). 208 | 209 | NB: Apple uses an NSString argument; you should never use a string argument! (c.f. top of this file for more details) 210 | 211 | ...instead, call this method for an entity named e.g. Entity (Apple's default entity name): 212 | 213 | "[stack fetchRequestForEntity:[Entity class]];" 214 | 215 | NB: Obviously, this implementation automatically uses the stack's current NSManagedObjectContext as the final argument - anything else wouldn't make sense 216 | */ 217 | -(NSFetchRequest*) fetchRequestForEntity:(Class) c; 218 | 219 | /** 220 | Shorthand for the most-frequently-used call to CoreData, that fetches a single object from the store. 221 | 222 | Ues your predicate to fetch an instance of the specified class. 223 | 224 | 1. If it finds more than one match (your predicate is wrong), returns nil AND a nil error. 225 | 2. If it hits an error in CoreData, it returns nil, and the error is passed-through from coredata. 226 | 3. Otherwise, it returns the object that matched 227 | 228 | This lets you write concise, clear source code, and if you were "expecting" only one object, but you got 229 | several, you will immediately get a nil - instead of an app that starts corrupting your database by 230 | writing to the wrong object. 231 | */ 232 | -(NSManagedObject*) fetchOneOrNil:(Class) c predicate:(NSPredicate*) predicate error:(NSError**) error; 233 | 234 | /** 235 | Shorthand to find out "how many objects are there in the database of class X / matching predicate Y?" 236 | 237 | Counts how many entities in CD match your predicate 238 | 239 | Returns -1 if it gets an error, or the number of matches otherwise 240 | */ 241 | -(int) countEntities:(Class) c matchingPredicate:(NSPredicate*) predicate; 242 | 243 | /** 244 | Delegates to fetchEntities:matchingPredicate:sortedByDescriptors: with a nil sortDescriptors array 245 | */ 246 | -(NSArray*) fetchEntities:(Class) c matchingPredicate:(NSPredicate*) predicate; 247 | 248 | /** 249 | Shorthand for doing arbitrary fetches from CoreData, in a single line of code, with type-safety (c.f. the top of this file for 250 | info on why Apple's original version is unsafe) 251 | 252 | Allows optional array of sort descriptors, to force CoreData to pre-sort the results for you; if you provide "nil" for sortDescriptors, 253 | it will do the default CoreData sort. 254 | 255 | Allows optional predicate, to force CoreData to filter using the predicate; if you provide "nil" for the predicate, it will 256 | return all entities unfiltered. 257 | 258 | Returns all entities of type Class in CD which match your predicate 259 | */ 260 | -(NSArray*) fetchEntities:(Class) c matchingPredicate:(NSPredicate*) predicate sortedByDescriptors:(NSArray*) sortDescriptors; 261 | 262 | /*! Deletes all data from your CoreData store - this is a very fast (and allegedly safe) way 263 | of resetting your data to a "virginal" state, as if your app had just been installed for the 264 | first time. 265 | 266 | It is ALMOST CERTAINLY not safe to call from a multithreaded environment, because nothing in 267 | Apple's code is threadsafe. But you already knew that, right? 268 | 269 | c.f. http://stackoverflow.com/questions/1077810/delete-reset-all-entries-in-core-data 270 | 271 | ALSO ... side effect: all NSFetchedResultsController's will explode. As of iOS 5.1, NSFetchedResultsController is 272 | buggy, and I'd recommend avoid using it if you possibly can. To make this slightly 273 | less painful, we'll post an NSNotificaiton that you can listen for inside your own NSFetchedResultsController 274 | subclasses, and re-build your NSFetchedResultsController when you receive one. 275 | 276 | [used in several apps live on AppStore already, to workaround the NSFetchedResultsController bugs] 277 | 278 | Easy way: use this code as your init method for your NSFetchedResultsController subclass 279 | 280 | -(id)initWithCoder:(NSCoder *)aDecoder 281 | { 282 | self = [super initWithCoder:aDecoder]; 283 | if (self) { 284 | // Custom initialization 285 | 286 | [[NSNotificationCenter defaultCenter] addObserverForName:kNotificationDestroyAllNSFetchedResultsControllers object:nil queue:nil usingBlock:^(NSNotification *note) { 287 | NSLog(@"[%@] must destroy my nsfetchedresultscontroller", [self class]); 288 | [__fetchedResultsController release]; 289 | __fetchedResultsController = nil; 290 | }]; 291 | } 292 | return self; 293 | } 294 | */ 295 | -(void) wipeAllData; 296 | 297 | /** remove current database / persistent store and replace with a new one. A backup of the old database will be saved at the same position with appended extension .old 298 | @param newDatabaseURL the NSURL of the new database / persistent store, pass nil if you want to reset the current persistent store without loading a new database. 299 | */ 300 | -(void)replaceDatabaseWithURL:(NSURL*)newDatabaseURL; 301 | 302 | /** resets the current database / persistent store. A backup of the old database will be saved at the same position with appended extension .old 303 | Convinience method for calling -replaceDatabaseWithURL:nil 304 | @see replaceDatabaseWithURL: 305 | */ 306 | -(void)resetDatabase; 307 | 308 | @end 309 | -------------------------------------------------------------------------------- /source/CoreDataStack.m: -------------------------------------------------------------------------------- 1 | /** 2 | CoreDataStack - CoreData made easy 3 | 4 | c.f. https://github.com/adamgit/CoreDataStack for docs + support 5 | */ 6 | 7 | #import "CoreDataStack.h" 8 | 9 | @interface CoreDataStack() 10 | + (NSURL *)applicationDocumentsDirectory; 11 | @property(nonatomic,assign,readwrite) NSThread* threadThatOwnsThisStack; 12 | @end 13 | 14 | @implementation CoreDataStack 15 | 16 | #pragma mark - @synthesize for old Xcode versions (pre Xcode 4.4) 17 | 18 | @synthesize databaseURL; 19 | @synthesize modelName; 20 | @synthesize coreDataStoreType; 21 | @synthesize automaticallyMigratePreviousCoreData; 22 | @synthesize defaultErrorBlock; 23 | 24 | #pragma mark - main class 25 | 26 | +(NSString*) sharedNameForModelName:(NSString*) mname dbName:(NSString*) dbname 27 | { 28 | return [NSString stringWithFormat:@"%@+%@", mname, dbname]; 29 | } 30 | 31 | #pragma mark - Initializers / Constructors 32 | 33 | +(CoreDataStack*) coreDataStackWithSharedModelName:(NSString *)mname databaseFilename:(NSString*) dbname 34 | { 35 | static NSMutableDictionary* sharedModelsByNameANdDBName; 36 | 37 | if( sharedModelsByNameANdDBName == nil ) 38 | { 39 | sharedModelsByNameANdDBName = [NSMutableDictionary new]; 40 | } 41 | 42 | CoreDataStack* sharedStack = [sharedModelsByNameANdDBName objectForKey:[self sharedNameForModelName:mname dbName:dbname]]; 43 | if( sharedStack == nil ) 44 | { 45 | sharedStack = [self coreDataStackWithModelName:mname databaseFilename:dbname]; 46 | [sharedModelsByNameANdDBName setObject:sharedStack forKey:[self sharedNameForModelName:mname dbName:dbname]]; 47 | } 48 | 49 | return sharedStack; 50 | } 51 | 52 | +(CoreDataStack*) coreDataStackWithModelName:(NSString *)mname databaseFilename:(NSString*) dbname 53 | { 54 | NSURL *storeURL; 55 | 56 | if( dbname != nil ) 57 | storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:dbname]; 58 | else 59 | storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:mname]; 60 | 61 | CoreDataStack* cds = [[[CoreDataStack alloc] initWithURL: storeURL 62 | modelName: mname 63 | storeType: CDSStoreTypeUnknown] autorelease]; 64 | 65 | return cds; 66 | } 67 | 68 | +(CoreDataStack*) coreDataStackWithModelName:(NSString *)mname 69 | { 70 | return [self coreDataStackWithModelName:mname databaseFilename:nil]; 71 | } 72 | 73 | +(CoreDataStack*) coreDataStackWithDatabaseURL:(NSURL*) dburl modelName:(NSString *)mname 74 | { 75 | CoreDataStack* cds = [[[CoreDataStack alloc] initWithURL:dburl modelName:mname storeType:CDSStoreTypeUnknown] autorelease]; 76 | 77 | return cds; 78 | } 79 | 80 | -(void) guessStoreType:(NSString*) fileExtension 81 | { 82 | if( fileExtension != nil && [fileExtension length] > 0) 83 | { 84 | /** Guess the Store Type */ 85 | if( [@"BINARY" isEqualToString:[fileExtension uppercaseString]] ) 86 | { 87 | self.coreDataStoreType = CDSStoreTypeBinary; 88 | } 89 | else if( [@"XML" isEqualToString:[fileExtension uppercaseString]] ) 90 | { 91 | self.coreDataStoreType = CDSStoreTypeXML; 92 | } 93 | else if( [@"SQL" isEqualToString:[fileExtension uppercaseString]] ) 94 | { 95 | self.coreDataStoreType = CDSStoreTypeSQL; 96 | } 97 | else if( [@"SQLITE" isEqualToString:[fileExtension uppercaseString]] ) 98 | { 99 | self.coreDataStoreType = CDSStoreTypeSQL; 100 | } 101 | else 102 | NSLog(@"[%@] WARN: no explicit store type given, and could NOT guess the store type. Core Data will PROBABLY refuse to initialize!", [self class] ); 103 | } 104 | } 105 | 106 | - (id)initWithURL:(NSURL*) url modelName:(NSString *)mname storeType:(CDSStoreType) type 107 | { 108 | self = [super init]; 109 | if (self) { 110 | self.databaseURL = url; 111 | self.modelName = mname; 112 | self.coreDataStoreType = type; 113 | self.automaticallyMigratePreviousCoreData = TRUE; 114 | 115 | if( self.coreDataStoreType == CDSStoreTypeUnknown ) 116 | { 117 | [self guessStoreType:[self.databaseURL pathExtension]]; 118 | } 119 | 120 | [self addObserver:self forKeyPath:@"managedObjectContextConcurrencyType" options:0 context:nil]; 121 | } 122 | return self; 123 | } 124 | 125 | - (void)dealloc 126 | { 127 | [self removeObserver:self forKeyPath:@"managedObjectContextConcurrencyType" context:nil]; 128 | 129 | self.threadThatOwnsThisStack = nil; 130 | self.databaseURL = nil; 131 | self.modelName = nil; 132 | 133 | [super dealloc]; 134 | } 135 | 136 | -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 137 | { 138 | if( [@"managedObjectContextConcurrencyType" isEqualToString:keyPath]) 139 | { 140 | NSAssert( _moc == nil, @"You cannot change the managedObjectContextConcurrencyType after you've started using the stack (you've already read the value of .managedObjectContext, I'm afraid) - that's going to make your app source code confused and broken and chaos"); 141 | } 142 | } 143 | 144 | #pragma mark - Apple core objects / references 145 | 146 | -(NSManagedObjectModel*) dataModel 147 | { 148 | if( _mom == nil ) 149 | { 150 | NSString* momdPath = [[NSBundle mainBundle]pathForResource:self.modelName ofType:@"momd"]; 151 | NSURL* momdURL = nil; 152 | 153 | /** 154 | New feature: WHEREVER your MOMD file is hiding, we'll find it! 155 | 156 | Problem: When you embed a CoreData project (e.g. a static library) inside another project (e.g. your app), 157 | Apple has banned us all from using Frameworks (even though Apple prefers us to write frameworks on OS X, 158 | they disabled them from Xcode when making iOS apps). 159 | 160 | You're supposed to attach multiple NSBundle's to your app - one for each 161 | subproject. 162 | 163 | This is great, but ... Apple provides no method to "search ALL bundles to find a file", they only provide 164 | a method to "search the top-level bundle AND IGNORE THE SUB-BUNDLES". 165 | 166 | So, if we fail to find the MOMD we're looking for, we'll look at each sub-bundle we find, and go looking in 167 | them for it. 168 | */ 169 | #define DEBUG_RECURSIVE_BUNDLE_SEARCHING 0 170 | if( momdPath == nil ) 171 | { 172 | #if DEBUG_RECURSIVE_BUNDLE_SEARCHING 173 | NSLog(@"[%@] WARN: Apple MOMD file was missing from main bundle. Now searching sub-bundles (1 level deep) to find it...", [self class]); 174 | #endif 175 | 176 | NSArray* allAppBundles = [NSBundle allBundles]; 177 | #if DEBUG_RECURSIVE_BUNDLE_SEARCHING 178 | NSLog( @"[%@] ... found %i potential app bundles that might contain it", [self class], allAppBundles.count); 179 | #endif 180 | for( NSBundle* appBundle in allAppBundles ) 181 | { 182 | momdURL = [appBundle URLForResource:self.modelName withExtension:@"momd" subdirectory:nil]; 183 | 184 | if( momdURL == nil ) // not found, so check the bundle for sub-bundles 185 | { 186 | for( NSURL* subURL in [appBundle URLsForResourcesWithExtension:@"bundle" subdirectory:nil]) 187 | { 188 | NSBundle* subBundle = [NSBundle bundleWithURL:subURL]; 189 | momdURL = [subBundle URLForResource:self.modelName withExtension:@"momd" subdirectory:nil]; 190 | if( momdURL != nil ) 191 | { 192 | break; 193 | } 194 | } 195 | } 196 | 197 | if( momdURL != nil ) 198 | { 199 | break; 200 | } 201 | } 202 | 203 | NSAssert( momdURL != nil, @"Failed to find the momd file for incoming modelName = %@. Maybe you forgot to convert your MOM to a MOMD? (Xcode major bug: used to do this automatically, now it doesn't)", self.modelName ); 204 | } 205 | else 206 | momdURL = [NSURL fileURLWithPath:momdPath]; 207 | 208 | _mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:momdURL]; 209 | } 210 | 211 | return _mom; 212 | } 213 | 214 | -(NSEntityDescription*) entityForClass:(Class) entityClass 215 | { 216 | NSEntityDescription* result = [NSEntityDescription entityForName:NSStringFromClass(entityClass) inManagedObjectContext:self.managedObjectContext]; 217 | 218 | return result; 219 | } 220 | 221 | -(NSManagedObject*) insertInstanceOfClass:(Class) entityClass 222 | { 223 | NSManagedObject* newObject = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(entityClass) inManagedObjectContext:self.managedObjectContext]; 224 | 225 | return newObject; 226 | } 227 | 228 | /** 229 | Returns the URL to the application's Documents directory. Used by Apple's reference code for finding the CoreData persistent files 230 | */ 231 | + (NSURL *)applicationDocumentsDirectory { 232 | return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; 233 | } 234 | 235 | -(NSPersistentStoreCoordinator*) persistentStoreCoordinator 236 | { 237 | if( _psc == nil ) 238 | { 239 | NSAssert( self.databaseURL != nil, @"This class should have been init'd with a URL, or with enough info to construct a valid URL" ); 240 | NSError *error = nil; 241 | _psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self dataModel]]; 242 | 243 | NSString* storeType; 244 | switch( self.coreDataStoreType ) 245 | { 246 | case CDSStoreTypeXML: 247 | { 248 | #ifdef NSXMLStoreType 249 | storeType = NSXMLStoreType; 250 | #else 251 | NSAssert( FALSE, @"Apple does not allow you to use an XML store on this OS. Only available on OS X" ); 252 | storeType = NSSQLiteStoreType; 253 | NSLog(@"[%@] ERROR: impossible store type. This type only exists on OS X. Using SqlLite instead ... %@", [self class], storeType ); 254 | #endif 255 | 256 | }break; 257 | 258 | case CDSStoreTypeBinary: 259 | { 260 | storeType = NSBinaryStoreType; 261 | }break; 262 | 263 | case CDSStoreTypeUnknown: 264 | { 265 | storeType = NSSQLiteStoreType; 266 | NSLog(@"[%@] WARN: unknown store type. Guessing ... %@", [self class], storeType ); 267 | }break; 268 | 269 | case CDSStoreTypeSQL: 270 | { 271 | storeType = NSSQLiteStoreType; 272 | }break; 273 | 274 | case CDSStoreTypeInMemory: 275 | { 276 | storeType = NSInMemoryStoreType; 277 | }break; 278 | } 279 | 280 | NSDictionary *options = nil; 281 | 282 | if( self.automaticallyMigratePreviousCoreData ) 283 | { 284 | options = [NSDictionary dictionaryWithObjectsAndKeys: 285 | [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, 286 | [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; 287 | } 288 | else 289 | NSLog(@"[%@] Warning: not migrating store (Apple default, but it's incorrect for 99%% of projects)", [self class]); 290 | 291 | if (![_psc addPersistentStoreWithType:storeType configuration:nil URL:self.databaseURL options:options error:&error]) { 292 | if (self.defaultErrorBlock) { 293 | self.defaultErrorBlock(self, error); 294 | } else { 295 | NSLog(@"[%@] Unresolved error %@, %@", [self class], error, [error userInfo]); 296 | abort(); 297 | } 298 | } 299 | } 300 | 301 | return _psc; 302 | } 303 | 304 | /** 305 | NB: all internal methods route via this method; this way, we can centrally check that you're not 306 | doing dangerous multi-threaded CoreData, and Assert if we catch you doing it 307 | 308 | NB: We "Assert" in this method because it is ALWAYS WRONG to do multi-threaded access against CoreData 309 | (apart from niche uses that are so specialized - and so hard to get right - that you won't see them 310 | on normal app projects) 311 | */ 312 | -(NSManagedObjectContext*) managedObjectContext 313 | { 314 | if( _moc == nil ) 315 | { 316 | _moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:self.managedObjectContextConcurrencyType]; 317 | [_moc setPersistentStoreCoordinator:[self persistentStoreCoordinator]]; 318 | 319 | NSLog(@"[%@] Info: Created a new NSManagedObjectContext (if you weren't expecting this, this could be fatal to your app", [self class] ); 320 | } 321 | 322 | if( self.threadThatOwnsThisStack == nil ) 323 | { 324 | self.threadThatOwnsThisStack = [NSThread currentThread]; 325 | } 326 | else 327 | { 328 | NSAssert( self.threadThatOwnsThisStack == [NSThread currentThread], @"FATAL ERROR: Apple's CoreData code is very UNSAFE with multithreading; you tried to access CoreData from thread = %@, but this stack was initialized by thread = %@. According to Apple, only the thread that initializes a ManagedObjectContext is allowed to read from or write to it", [NSThread currentThread], self.threadThatOwnsThisStack ); 329 | } 330 | 331 | return _moc; 332 | } 333 | 334 | #pragma mark - Convenience methods that can be implemented generically on top of any coredata stack 335 | 336 | -(NSFetchRequest*) fetchRequestForEntity:(Class) c 337 | { 338 | return [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass(c)]; 339 | } 340 | 341 | -(NSManagedObject*) fetchOneOrNil:(Class) c predicate:(NSPredicate*) predicate error:(NSError**) error 342 | { 343 | NSFetchRequest* fetch = [self fetchRequestForEntity:c]; 344 | fetch.predicate = predicate; 345 | NSArray* result = [self.managedObjectContext executeFetchRequest:fetch error:error]; 346 | 347 | if( result == nil ) 348 | { 349 | return nil; 350 | } 351 | else if( result.count != 1 ) 352 | { 353 | if( error != nil ) 354 | *error = nil; // not an error, but wrong number of matches 355 | return nil; 356 | } 357 | else 358 | { 359 | if( error != nil ) 360 | *error = nil; 361 | return [result objectAtIndex:0]; 362 | } 363 | } 364 | 365 | -(BOOL) storeContainsAtLeastOneEntityOfClass:(Class) c 366 | { 367 | NSFetchRequest* fetchAny = [self fetchRequestForEntity:c]; 368 | NSError* error; 369 | NSArray* anyCats = [self.managedObjectContext executeFetchRequest:fetchAny error:&error]; 370 | if (error && self.defaultErrorBlock) defaultErrorBlock(self, error); 371 | if( [anyCats count] > 0 ) 372 | return TRUE; 373 | 374 | return FALSE; 375 | } 376 | 377 | -(NSArray*) fetchEntities:(Class) c matchingPredicate:(NSPredicate*) predicate 378 | { 379 | return [self fetchEntities:c matchingPredicate:predicate sortedByDescriptors:nil]; 380 | } 381 | 382 | -(NSArray*) fetchEntities:(Class) c matchingPredicate:(NSPredicate*) predicate sortedByDescriptors:(NSArray*) sortDescriptors 383 | { 384 | NSFetchRequest* fetch = [self fetchRequestForEntity:c]; 385 | if( predicate != nil ) 386 | fetch.predicate = predicate; 387 | if( sortDescriptors != nil ) 388 | fetch.sortDescriptors = sortDescriptors; 389 | NSError* error; 390 | NSArray* result = [self.managedObjectContext executeFetchRequest:fetch error:&error]; 391 | 392 | if( result == nil ) 393 | { 394 | if (error && self.defaultErrorBlock) defaultErrorBlock(self, error); 395 | NSLog(@"[%@] ERROR calling fetchEntities:matchingPredicate for predicate %@, error = %@", [self class], predicate, error ); 396 | return nil; 397 | } 398 | else 399 | return result; 400 | } 401 | 402 | -(int) countEntities:(Class) c matchingPredicate:(NSPredicate*) predicate 403 | { 404 | NSFetchRequest* fetch = [self fetchRequestForEntity:c]; 405 | fetch.predicate = predicate; 406 | NSError* error; 407 | NSArray* result = [self.managedObjectContext executeFetchRequest:fetch error:&error]; 408 | 409 | if( result == nil ) 410 | { 411 | NSLog(@"[%@] ERROR calling countEntities:matchingPredicate for predicate %@, error = %@", [self class], predicate, error ); 412 | if (error && self.defaultErrorBlock) defaultErrorBlock(self, error); 413 | return -1; 414 | } 415 | else 416 | return (int)result.count; 417 | } 418 | 419 | -(void) saveOrFail:(void(^)(NSError* errorOrNil)) blockFailedToSave 420 | { 421 | NSError* error = nil; 422 | if( [[self managedObjectContext] save:&error] ) 423 | { 424 | return; 425 | } 426 | else 427 | { 428 | if (!blockFailedToSave && self.defaultErrorBlock) { 429 | if( [self managedObjectContext] == nil ) 430 | { 431 | self.defaultErrorBlock(self, [NSError errorWithDomain:@"CoreDataStack" code:1 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Attempted to save a nil NSManagedObjectContext. This CoreDataStack has no context - probably there was an earlier error trying to access the CoreData database file", NSLocalizedDescriptionKey, nil]]); 432 | } 433 | else 434 | { 435 | self.defaultErrorBlock(self, error); 436 | 437 | if( self.shouldAssertWhenSaveFails ) 438 | NSAssert( FALSE, @"A CoreData save failed, and you asked me to Assert when this happens. This is a very serious error - you should investigate!"); 439 | } 440 | 441 | } else { 442 | if( [self managedObjectContext] == nil ) 443 | { 444 | blockFailedToSave( [NSError errorWithDomain:@"CoreDataStack" code:1 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Attempted to save a nil NSManagedObjectContext. This CoreDataStack has no context - probably there was an earlier error trying to access the CoreData database file", NSLocalizedDescriptionKey, nil]] ); 445 | } 446 | else 447 | { 448 | blockFailedToSave( error ); 449 | 450 | if( self.shouldAssertWhenSaveFails ) 451 | NSAssert( FALSE, @"A CoreData save failed, and you asked me to Assert when this happens. This is a very serious error - you should investigate!"); 452 | } 453 | } 454 | } 455 | } 456 | 457 | -(void) wipeAllData 458 | { 459 | for( NSPersistentStore* store in [self.persistentStoreCoordinator persistentStores] ) 460 | { 461 | NSError *error; 462 | NSURL *storeURL = store.URL; 463 | [self.persistentStoreCoordinator removePersistentStore:store error:&error]; 464 | [[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error]; 465 | _moc = nil; 466 | _mom = nil; 467 | _psc = nil; 468 | 469 | if (error && self.defaultErrorBlock) defaultErrorBlock(self, error); 470 | 471 | /** ... side effect: all NSFetchedResultsController's will now explode because Apple didn't code them very well */ 472 | [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDestroyAllNSFetchedResultsControllers object:self]; 473 | } 474 | } 475 | 476 | -(void)replaceDatabaseWithURL:(NSURL *)newDatabaseURL { 477 | [_moc reset]; 478 | NSError *error; 479 | for (NSPersistentStore *store in _psc.persistentStores) { 480 | BOOL removed = [_psc removePersistentStore:store error:&error]; 481 | if (!removed) { 482 | NSLog(@"Unable to remove persistent store: %@", error); 483 | } 484 | } 485 | if (newDatabaseURL) { 486 | BOOL moved = [[NSFileManager defaultManager] replaceItemAtURL:self.databaseURL 487 | withItemAtURL:newDatabaseURL 488 | backupItemName:[[self.databaseURL lastPathComponent] stringByAppendingPathExtension:@"old"] 489 | options:0 490 | resultingItemURL:nil 491 | error:&error]; 492 | if (!moved) { 493 | NSLog(@"Unable to move temporary store: %@", error); 494 | } 495 | } else { 496 | [[NSFileManager defaultManager] moveItemAtURL:self.databaseURL toURL:[self.databaseURL URLByAppendingPathExtension:@"old"] error:&error]; 497 | } 498 | 499 | NSDictionary* options = nil; 500 | if (self.automaticallyMigratePreviousCoreData) { 501 | options = @{NSMigratePersistentStoresAutomaticallyOption : @YES, 502 | NSInferMappingModelAutomaticallyOption : @YES 503 | }; 504 | } 505 | NSPersistentStore *persistentStore = [_psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.databaseURL options:options error:&error]; 506 | if (!persistentStore) { 507 | NSLog(@"Unable to add new store: %@", error); 508 | } 509 | [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationDestroyAllNSFetchedResultsControllers object:self]; 510 | if (error && self.defaultErrorBlock) { 511 | self.defaultErrorBlock(self, error); 512 | } 513 | } 514 | 515 | -(void)resetDatabase { 516 | [self replaceDatabaseWithURL:nil]; 517 | } 518 | 519 | @end 520 | --------------------------------------------------------------------------------