Stack management, ActiveRecord utilities, and seamless importing for Core Data.
Add the following to your Podfile:
pod RZVinyl
To exclude RZImport extensions, use the Core
subspec:
pod RZVinyl/Core
RZVinyl follows semantic versioning conventions. As newer versions are released, you will need to update the version spec in your Podfile as necessary. See the release history for version information and release notes.
Simply copy/add the contents of Classes/
into your project and ensure that you are linking with Core Data. Do not copy the contents of Extensions/
.
Because of the optional RZImport extensions, which depend on the RZImport library, manual installation is a bit more difficult. Hence why CocoaPods is the recommended installation method.
To install manually with RZImport extensions:
- Follow the steps for installing without import extensions.
- Also copy the contents of
Extensions/
into your project. - Install RZImport in your project.
- Add the following to your build configuration's compiler flags:
-DRZV_IMPORT_AVAILABLE=1
If all went well, your project should build cleanly and the methods from NSManagedObject+RZImport.h
should be available.
A demo project is available in the Example
directory. The demo project uses CocoaPods, and can be opened from a temporary directory by running
pod try RZVinyl
Alternatively, the demo can be configured by running the following commands from the root project directory.
cd Example
pod install
Then, open RZVinylDemo.xcworkspace
and check out the demo!
Note: The above steps assume that the CocoaPods gem is installed.
If you do not have CocoaPods installed, follow the instructions here.
RZVinyl is fully compatible with Swift, and makes use of nullability annotations and lightweight generics where appropriate.
To use RZVinyl, first add #import "RZVinyl.h"
to any classes that will need to use it, or to your app's .pch
file. RZVinyl can be broken down into three basic areas of functionality, as follows.
RZCoreDataStack
is a class for building and managing Core Data stacks, including the model, managed object context, and persistent store coordinator. It has convenience methods for performing concurrent background operations with a separate managed object context, as well as a set of class methods for making a default stack available via the singleton pattern.
// Default configuration, default store URL (in Library directory), no extra options
RZCoreDataStack *myStack = [[RZCoreDataStack alloc] initWithModelName:@"MyModel"
configuration:nil
storeType:NSInMemoryStoreType
storeURL:nil
options:kNilOptions];
[myStack performBlockUsingBackgroundContext:^(NSManagedObjectContext *context) {
// do some stuff with the context
// this block is on a background thread and the context has private-queue confinement
} completion:^(NSError *err) {
// this block is on the main thread
}];
Each managed object subclass can provide a "stale" predicate that will be used here to delete all objects which pass the predicate. This is useful for cleaning up stale or orphaned objects. This can also be invoked every time the app enters the background if you initialize the stack with the RZCoreDataStackOptionsEnableAutoStalePurge
option.
[myStack purgeStaleObjectsWithCompletion:^(NSError *err){
// This is on the main thread
if ( !err ) {
NSLog(@"Purged!");
}
}];
// This context is a child of the disk writer context and a sibling of the main thread context.
// Saving it will automatically merge changes into the main context.
NSManagedObjectContext *backgroundContext = [myStack backgroundManagedObjectContext];
// This context is a child of the primary main thread context.
// It is useful for creating a "sandbox" for temporary edits that may or may not be saved.
// Its objects can safely be used on the main thread (e.g. with UI elements).
// Saving it will automatically push changes up to the primary main thread context.
NSManagedObjectContext *scratchContext = [myStack temporaryManagedObjectContext];
RZVinylRecord
is a category on NSManagedObject
which provides a partial implementation of the Active Record pattern. Each method in NSManagedObject+RZVinylRecord
has two signatures - one which accepts a managed object context parameter, and one which uses the main managed object context from the default RZCoreDataStack
.
// Delete all objects of receiver's type in default stack's main context
+ (void)rzv_deleteAll;
// Delete all objects of receiver's type in the provided context
+ (void)rzv_deleteAllInContext:(NSManagedObjectContext *)context;
The no-context versions may only be used from the main thread, or an exception will be thrown. Similarly, if no default stack has been set, attempting to call one of these methods without providing a context will also throw an exception.
// Inserted into the default main context
MyManagedObject *newObject = [MyManagedObject rzv_newObject];
// Inserted into the provided context
MyManagedObject *newObject = [MyManagedObject rzv_newObjectInContext:context];
These methods use the attribute provided by implementing + (NSString *)rzv_primaryKey;
in the managed object subclass to search for an existing object with the provided value for that attribute, optionally creating a new object and initializing it with the primary key value if one was not found.
// In the default main context
MyManagedObject *existingObjectOrNil = [MyManagedObject rzv_objectWithPrimaryKeyValue:@(12345) createNew:NO];
// In the provided context, creating a new instance if one isn't found
MyManagedObject *existingObjectOrNil = [MyManagedObject rzv_objectWithPrimaryKeyValue:@(12345) createNew:YES inContext:context];
You can also find/create objects based on a set of other attributes. If createNew
is YES and a match isn't found, a new instance will be created and initialized with the provided attribute dictionary.
// In the default main context
MyManagedObject *existingObjectOrNil = [MyManagedObject rzv_objectWithAttributes:@{ @"name" : @"Bob Marley" }
createNew:NO];
// In the provided context, creating a new instance if one isn't found
MyManagedObject *existingObjectOrNil = [MyManagedObject rzv_objectWithAttributes:@{ @"name" : @"Bob Marley" }
createNew:YES
inContext:context];
// In the default main context
NSArray *allMyObjects = [MyManagedObject rzv_all];
// In the provided context
NSArray *allMyObjects = [MyManagedObject rzv_allInContext:context];
// In the default main context
NSArray *matchingObjects = [MyManagedObject rzv_where:RZVPred(@"someAttribute >= 18")];
// In the provided context
NSArray *matchingObjects = [MyManagedObject rzv_where:RZVPred(@"someAttribute >= 18") inContext:context];
The "where" methods also have versions that take sort descriptors to sort the results.
// In the default main context
NSUInteger theCount = [MyManagedObject rzv_count];
// In the provided context, with a predicate
NSUInteger theCount = [MyManagedObject rzv_countWhere:RZVPred(@"someAttribute >= %@", minimum)
inContext:context];
// Uses the object's context
[myObjectInstance rzv_delete];
// In the default main context
[MyManagedObject rzv_deleteAll];
// In the provided context, with a predicate
[MyManagedObject rzv_deleteAllWhere:RZVPred(@"someAttribute == nil") inContext:context];
The semantics of saving an object in CoreData are rather different from what might be expected when using the Active Record pattern, particularly when dealing with a more complex context hierarchy, as in RZCoreDataStack
. In order to persist changes to the persistent store, it is necessary to save the entire context tree all the way to its root, which also saves any other changes in any of the contexts along the way. To avoid unintended, non-obvious consequences, no "save" methods are provided for managed object classes via RZVinylRecord
, and saving must be handled via managed object contexts themselves.
To facilitate this, NSManagedObjectContext+RZVinylSave
provides two methods for saving up the context tree all the way to the persistent store, in synchronous and asynchronous flavors.
NSError *saveError = nil;
if ( ![context rzv_saveToStoreAndWait:&saveError] ) {
NSLog(@"Error saving context: %@", saveError);
}
[context rzv_saveToStoreWithCompletion:^(NSError *error){
// Called on main thread
if ( error ) {
NSLog(@"Error saving context: %@", saveError);
}
}];
RZImport can be combined with RZVinyl for powerful automatic importing of managed objects from deserialized JSON or any other plain-ol-data NSDictionary
or NSArray
source.
NSManagedObject+RZImport
provides a partial implementation of the RZImportable
protocol that automatically handles object uniquing and relationship imports. For a working demo, see the example project.
To enable RZImport for your managed object subclasses, create a category and implement the following methods from RZVinylRecord
and RZVinylRip
informal protocols:
Implement to return the name of the property attributes representing the "unique ID" of this object type
If the key in the dictionary representations of this object is different from the primary key property name, implement this method to return that key here. If not implemented, the same value returned by rzv_primaryKey
will be used to find unique instances of the object.
You can also implement the methods of RZImportable
in your managed object classes to handle validation, provide further custom key/property mappings, etc, with two important caveats:
This is implemented by the NSManagedObject+RZImport
category to handle Core Data concurrency, and internally calls the extended version with a context parameter:
+ (id)rzi_existingObjectForDict:(NSDictionary *)dict inContext:(NSManagedObjectContext *)context;
The implementation provided by the category automatically manages unique objects during an import by finding an existing object matching the dictionary being imported. This default implementation is safe to override as long as you always return the value provided by super
in the cases that your override does not handle.
The category implementation handles recursive imports for keys representing relationships. You can override the method in a subclass, as long as you return the result of invoking the super
implementation for keys that your override does not handle. See below for an example.
Here is an example of a managed object subclass that is configured for usage with RZImport
.
RZArtist.h
@interface RZArtist : NSManagedObject
@property (nonatomic, retain) NSNumber *remoteID;
@property (nonatomic, retain) NSDate *birthdate;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *rating;
@property (nonatomic, retain) RZGenre *genre; // one-to-one relationship
@property (nonatomic, retain) NSSet *songs; // one-to-many relationship of 'RZSong' objects
@end
RZArtist+RZImport.h
@interface RZArtist (RZImport) <RZImportable>
@end
RZArtist+RZImport.m
@implementation RZArtist (RZImport)
+ (NSString *)rzv_primaryKey
{
return @"remoteID";
}
+ (NSString *)rzv_externalPrimaryKey
{
return @"id";
}
+ (NSDictionary *)rzi_customMappings
{
return @{ @"dob" : @"birthdate" };
}
+ (NSString *)rzi_dateFormatForKey:(NSString *)key
{
if ( [key isEqualToString:@"dob"] ) {
return @"yyyy-MM-dd";
}
return nil;
}
- (BOOL)rzi_shouldImportValue:(id)value forKey:(NSString *)key
{
// Genre string will be imported as a managed object (RZGenre)
if ( [key isEqualToString:@"genre"] ) {
// Could also use an NSValueTransformer if this will be done in multiple classes
if ( [value isKindOfClass:[NSString class]] ) {
self.genre = [RZGenre rzv_objectWithAttributes:@{ @"name" : value }
createNew:YES
inContext:self.managedObjectContext];
}
return NO;
}
return [super rzi_shouldImportValue:value forKey:key];
}
@end
Using this basic implementation and assuming RZSong
and RZGenre
are also configured correctly, you can do the following:
// This could just as easily be deserialized JSON
NSDictionary *artistDict = @{
@"id" : @100,
@"dob" : @"1942-11-27", // string -> date, via provided format
@"rating" : @"4.7", // string -> number, automatically
@"name" : @"Jimi Hendrix",
@"genre" : @"Psychedelic Rock", // string -> RZGenre, via protocol method
@"songs" : @[ // array -> RZSong to-many relationship, automatically
@{
@"id" : @1000,
@"title" : @"Hey Joe"
},
@{
@"id" : @1001,
@"title" : @"Spanish Castle Magic"
}
]
};
[[RZCoreDataStack defaultStack] performBlockUsingBackgroundContext:^(NSManagedObjectContext *context) {
// Import jimi and his nested songs from the dictionary
RZArtist *jimi = [RZArtist rzi_objectFromDictionary:artistDict inContext:context];
} completion:^(NSError *err) {
if ( !err ) {
[myStack save:YES];
}
// Fetch the record from the main thread
RZArtist *mainThreadJimi = [RZArtist rzv_objectWithPrimaryKeyValue:@100];
}];
For more comprehensive documentation, see the CococaDocs page.
RZVinyl is licensed under the MIT license. See the LICENSE
file for details.