YapDatabase/Examples/MyDatabaseObjectExample/MyDatabaseObject.m
2015-03-17 18:59:49 -07:00

583 lines
18 KiB
Objective-C

#import "MyDatabaseObject.h"
#import <objc/runtime.h>
@implementation MyDatabaseObject {
@private
BOOL isImmutable;
NSMutableSet *changedProperties;
NSMutableDictionary *originalCloudValues;
}
/**
* Make sure all your subclasses call this method ([super init]).
**/
- (instancetype)init
{
if ((self = [super init]))
{
// Turn on KVO for object.
// We do this so we can get notified if the user is about to make changes to one of the object's properties.
//
// Don't worry, this doesn't create a retain cycle.
[self addObserver:self forKeyPath:@"isImmutable" options:0 context:NULL];
if ([[self class] storesOriginalCloudValues]) {
originalCloudValues = [[NSMutableDictionary alloc] init];
}
}
return self;
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:@"isImmutable" context:NULL];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark NSCopying
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* In this example, all copies are automatically mutable.
* So all you have to do in your code is something like this:
*
* [databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction]{
*
* Car *car = [transaction objectForKey:carId inCollection:@"cars"];
* car = [car copy]; // make mutable copy
* car.speed = newSpeed;
*
* [transaction setObject:car forKey:carId inCollection:@"cars"];
* }];
*
* Which means all you have to do is implement the copyWithZone method in your model classes.
**/
- (id)copyWithZone:(NSZone *)zone
{
// Subclasses should call this method via [super copyWithZone:zone].
// For example:
//
// MySubclass *copy = [super copyWithZone:zone];
// copy->ivar1 = [ivar1 copy];
// copy->ivar2 = ivar2;
// return copy;
MyDatabaseObject *copy = [[[self class] alloc] init];
copy->isImmutable = NO;
copy->changedProperties = [self->changedProperties mutableCopy];
copy->originalCloudValues = [self->originalCloudValues mutableCopy];
return copy;
}
/**
* An alternative is to have [object copy] return an immutable copy,
* and [object mutableCopy] to return a mutable copy.
*
* Some people prefer it like this. If so then:
* - uncomment this method
* - change 'copy->isImmutable = NO' to 'copy->isImmutable = YES' in copyWithZone
* - and add NSMutableCopying to the list of protocols in the header file
*
* Note: The implemenation below just uses a regular copy, and then sets the isImmutable flag to NO.
* So if you go this route, you don't have to implement mutableCopyWithZone (just copyWithZone).
**/
//- (instancetype)mutableCopyWithZone:(NSZone *)zone
//{
// MyDatabaseObject *copy = [self copy];
// copy->isImmutable = NO;
//
// return copy;
//}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Class Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* This method returns a list of all properties that should be monitored.
* That is, these properties should show up in the changedProperties set if they are modified.
* And they should be considered immutable once the makeImmutable method has been invoked.
*
* By default this method returns a list of all properties in each subclass in the
* hierarchy leading to "[self class]".
*
* However, this is not always exactly what you want.
* For example, you may have properties which are simply used for caching:
*
* @property (nonatomic, strong, readwrite) UIImage *avatarImage;
* @property (nonatomic, strong, readwrite) UIImage *cachedTransformedAvatarImage;
*
* In this example, you store the user's plain avatar image.
* However, your code transforms the avatar in various ways for display in the UI.
* So to reduce overhead, you'd like to cache these transformed images in the user object.
* Thus the 'cachedTransformedAvatarImage' property doesn't actually mutate the user object. It's just a temp cache.
*
* So your subclass would override this method like so:
*
* + (NSMutableSet *)monitoredProperties
* {
* NSMutableSet *monitoredProperties = [super immutableProperties];
* [monitoredProperties removeObject:@"cachedTransformedAvatarImage"];
*
* return monitoredProperties;
* }
**/
+ (NSMutableSet *)monitoredProperties
{
// Steps to override me (if needed):
//
// - Invoke [super monitoredProperties]
// - Modify resulting mutable set
// - Return modified set
NSMutableSet *properties = nil;
Class rootClass = [MyDatabaseObject class];
Class subClass = [self class];
while (subClass != rootClass)
{
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList(subClass, &count);
if (propertyList)
{
if (properties == nil)
properties = [NSMutableSet setWithCapacity:count];
for (unsigned int i = 0; i < count; i++)
{
const char *name = property_getName(propertyList[i]);
NSString *property = [NSString stringWithUTF8String:name];
[properties addObject:property];
}
free(propertyList);
}
subClass = [subClass superclass];
}
return properties;
}
/**
* Generally you should NOT override this method.
* Just override the class version of this method (above).
**/
- (NSSet *)monitoredProperties
{
NSSet *cached = objc_getAssociatedObject([self class], _cmd);
if (cached) return cached;
NSSet *monitoredProperties = [[[self class] monitoredProperties] copy];
objc_setAssociatedObject([self class], _cmd, monitoredProperties, OBJC_ASSOCIATION_RETAIN);
return monitoredProperties;
}
/**
* This method returns a mapping from localPropertyName to cloudPropertyName.
*
* By default this method returns a dictionary including everything in [self monitoredProperties],
* where the key is equal to the value for every item.
*
* For example:
* @{ @"title" : @"title",
* @"isComplete" : @"isComplete",
* @"creationDate" : @"creationDate",
* @"lastModified" : @"lastModified",
* @"isSeen" : @"isSeen"
* }
*
* However, this is not always exactly what you want.
* For example, you may not want to sync the 'isSeen' property because it's device-specific.
*
* Additionally you discover that CKRecord has a built-in creationDate property,
* and so CKRecord doesn't allow you to use that key for your own purposes. (It's reserved.)
*
* You can still name your own property "creationDate",
* but you'll be forced to use a different name for the CKRecord.
* So let's say we decide to use "created" as the corresponding key in the CKRecord.
*
* Thus your subclass overrides this method like so:
*
* + (NSMutableDictionary *)mappings_localKeyToCloudKey
* {
* NSMutableDictionary *mappings_localKeyToCloudKey = [super mappings_localKeyToCloudKey];
*
* [mappings_localKeyToCloudKey removeObjectForKey:@"isSeen"];
* [mappings_localKeyToCloudKey setObject:@"created" forKey:@"creationDate"];
*
* return mappings_localKeyToCloudKey;
* }
**/
+ (NSMutableDictionary *)mappings_localKeyToCloudKey
{
// Steps to override me (if needed):
//
// - Invoke [super mappings_localKeyToCloudKey]
// - Modify resulting mutable dictionary
// - Return modified dictionary
NSMutableSet *properties = [self monitoredProperties];
NSMutableDictionary *syncablePropertyMappings = [NSMutableDictionary dictionaryWithCapacity:properties.count];
for (NSString *propertyName in properties)
{
[syncablePropertyMappings setObject:propertyName forKey:propertyName];
}
return syncablePropertyMappings;
}
/**
* Generally you should NOT override this method.
* Just override the class version of this method (above).
**/
- (NSDictionary *)mappings_localKeyToCloudKey
{
NSDictionary *cached = objc_getAssociatedObject([self class], _cmd);
if (cached) return cached;
NSDictionary *mappings_localKeyToCloudKey = [[[self class] mappings_localKeyToCloudKey] copy];
objc_setAssociatedObject([self class], _cmd, mappings_localKeyToCloudKey, OBJC_ASSOCIATION_RETAIN);
return mappings_localKeyToCloudKey;
}
/**
* This method is the inverse of mappings_localKeyToCloudKey.
* There is generally no need to override this method.
**/
+ (NSMutableDictionary *)mappings_cloudKeyToLocalKey
{
NSMutableDictionary *mappings_localKeyToCloudKey = [self mappings_localKeyToCloudKey];
NSUInteger capacity = mappings_localKeyToCloudKey.count;
NSMutableDictionary *mappings_cloudKeyToLocalKey = [NSMutableDictionary dictionaryWithCapacity:capacity];
[mappings_localKeyToCloudKey enumerateKeysAndObjectsUsingBlock:^(id localKey, id cloudKey, BOOL *stop) {
mappings_cloudKeyToLocalKey[cloudKey] = localKey;
}];
return mappings_cloudKeyToLocalKey;
}
/**
* There is generally no need to override this method.
**/
- (NSDictionary *)mappings_cloudKeyToLocalKey
{
NSDictionary *cached = objc_getAssociatedObject([self class], _cmd);
if (cached) return cached;
NSDictionary *mappings_cloudKeyToLocalKey = [[[self class] mappings_cloudKeyToLocalKey] copy];
objc_setAssociatedObject([self class], _cmd, mappings_cloudKeyToLocalKey, OBJC_ASSOCIATION_RETAIN);
return mappings_cloudKeyToLocalKey;
}
/**
* If storesOriginalCloudValues is enabled, then in addition to monitoring which properties change,
* the object will also keep a dictionary of the original cloudValues that have changed.
*
* This is disabled by default.
* So you'll need to "opt-in" for those classes where you want this feature.
**/
+ (BOOL)storesOriginalCloudValues
{
// Override me (and return YES), if you want to store originalCloudValues.
// These are cleared when clearChangedProperties is invoked.
return NO;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Immutability
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@synthesize isImmutable = isImmutable;
- (void)makeImmutable
{
if (!isImmutable)
{
// Set immutable flag
isImmutable = YES;
}
}
- (NSException *)immutableExceptionForKey:(NSString *)key
{
NSString *reason;
if (key)
reason = [NSString stringWithFormat:
@"Attempting to mutate immutable object. Class = %@, property = %@", NSStringFromClass([self class]), key];
else
reason = [NSString stringWithFormat:
@"Attempting to mutate immutable object. Class = %@", NSStringFromClass([self class])];
NSDictionary *userInfo = @{ NSLocalizedRecoverySuggestionErrorKey:
@"To make modifications you should create a copy via [object copy]."
@" You may then make changes to the copy before saving it back to the database."};
return [NSException exceptionWithName:@"STDatabaseObjectException" reason:reason userInfo:userInfo];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Monitoring (local)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSSet *)changedProperties
{
if ([changedProperties count] == 0) return nil;
// Remove untracked properties from the list.
[changedProperties intersectSet:[self monitoredProperties]];
// And return immutable copy
return [changedProperties copy];
}
- (BOOL)hasChangedProperties
{
return ([changedProperties count] > 0);
}
- (void)clearChangedProperties
{
changedProperties = nil;
[originalCloudValues removeAllObjects];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Monitoring (cloud)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSSet *)allCloudProperties
{
NSSet *cached = objc_getAssociatedObject([self class], _cmd);
if (cached) return cached;
NSDictionary *mappings_localKeyToCloudKey = self.mappings_localKeyToCloudKey;
NSUInteger capacity = mappings_localKeyToCloudKey.count;
NSMutableSet *allCloudProperties = [NSMutableSet setWithCapacity:capacity];
for (NSString *cloudKey in [mappings_localKeyToCloudKey objectEnumerator])
{
[allCloudProperties addObject:cloudKey];
}
NSSet *result = [allCloudProperties copy];
objc_setAssociatedObject([self class], _cmd, result, OBJC_ASSOCIATION_RETAIN);
return result;
}
- (NSSet *)changedCloudProperties
{
if ([changedProperties count] == 0) return nil;
NSMutableSet *changedCloudProperties = [NSMutableSet setWithCapacity:changedProperties.count];
NSDictionary *mappings_localKeyToCloudKey = self.mappings_localKeyToCloudKey;
for (NSString *localKey in changedProperties)
{
NSString *cloudKey = mappings_localKeyToCloudKey[localKey];
if (cloudKey) {
[changedCloudProperties addObject:cloudKey];
}
}
return changedCloudProperties;
}
- (BOOL)hasChangedCloudProperties
{
if ([changedProperties count] == 0) return NO;
NSDictionary *mappings_localKeyToCloudKey = self.mappings_localKeyToCloudKey;
for (NSString *localKey in changedProperties)
{
NSString *cloudKey = mappings_localKeyToCloudKey[localKey];
if (cloudKey) {
return YES;
}
}
return NO;
}
- (NSDictionary *)originalCloudValues
{
return [originalCloudValues copy];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Getters & Setters (cloud)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSString *)cloudKeyForLocalKey:(NSString *)localKey
{
return self.mappings_localKeyToCloudKey[localKey];
}
- (NSString *)localKeyForCloudKey:(NSString *)cloudKey
{
return self.mappings_cloudKeyToLocalKey[cloudKey];
}
- (id)cloudValueForCloudKey:(NSString *)cloudKey
{
// Override me if needed.
// For example:
//
// - (id)cloudValueForCloudKey:(NSString *)cloudKey
// {
// if ([cloudKey isEqualToString:@"color"])
// {
// // We store UIColor in the cloud as a string (r,g,b,a)
// return ConvertUIColorToNSString(self.color);
// }
// else
// {
// return [super cloudValueForCloudKey:cloudKey];
// }
// }
return [self localValueForCloudKey:cloudKey];
}
- (id)cloudValueForLocalKey:(NSString *)localKey
{
NSString *cloudKey = [self cloudKeyForLocalKey:localKey];
return [self cloudValueForCloudKey:cloudKey];
}
- (id)localValueForCloudKey:(NSString *)cloudKey
{
NSString *localKey = [self localKeyForCloudKey:cloudKey];
return [self valueForKey:localKey];
}
- (id)localValueForLocalKey:(NSString *)localKey
{
return [self valueForKey:localKey];
}
- (void)setLocalValueFromCloudValue:(id)cloudValue forCloudKey:(NSString *)cloudKey
{
// Override me if needed.
// For example:
//
// - (void)setLocalValueFromCloudValue:(id)cloudValue forCloudKey:(NSString *)cloudKey
// {
// if ([cloudKey isEqualToString:@"color"])
// {
// // We store UIColor in the cloud as a string (r,g,b,a)
// self.color = ConvertNSStringToUIColor(cloudValue);
// }
// else
// {
// return [super setLocalValueForCloudValue:cloudValue cloudKey:cloudKey];
// }
// }
NSString *localKey = [self localKeyForCloudKey:cloudKey];
[self setValue:cloudValue forKey:localKey];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark KVO
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"isImmutable"])
return YES;
else
return [super automaticallyNotifiesObserversForKey:key];
}
+ (NSSet *)keyPathsForValuesAffectingIsImmutable
{
// In order for the KVO magic to work, we specify that the isImmutable property is dependent
// upon all other properties in the class that should become immutable.
//
// The code below ** attempts ** to do this automatically.
// It does so by creating a list of all the properties in the class.
//
// Obviously this will not work for every situation.
// In particular:
//
// - if you have custom setter methods that aren't specified as properties
// - if you have other custom methods that modify the object
//
// To cover these edge cases, simply add code like the following at the beginning of such methods:
//
// - (void)recalculateFoo
// {
// if (self.isImmutable) {
// @throw [self immutableExceptionForKey:@"foo"];
// }
//
// // ... normal code that modifies foo ivar ...
// }
return [self monitoredProperties];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Nothing to do (but method is required to exist)
}
- (void)willChangeValueForKey:(NSString *)key
{
if (isImmutable)
{
@throw [self immutableExceptionForKey:key];
}
if (originalCloudValues)
{
NSString *cloudKey = [self cloudKeyForLocalKey:key];
if ([self.allCloudProperties containsObject:cloudKey])
{
if (!CFDictionaryContainsKey((CFDictionaryRef)originalCloudValues, (const void *)cloudKey))
{
id originalCloudValue = [self cloudValueForCloudKey:cloudKey];
if (originalCloudValue) {
[originalCloudValues setObject:originalCloudValue forKey:cloudKey];
}
}
}
}
[super willChangeValueForKey:key];
}
- (void)didChangeValueForKey:(NSString *)key
{
if (changedProperties == nil)
changedProperties = [[NSMutableSet alloc] init];
[changedProperties addObject:key];
[super didChangeValueForKey:key];
}
@end