YapDatabase/Examples/MyDatabaseObjectExample/MyDatabaseObject.m

281 lines
7.7 KiB
Objective-C

#import "MyDatabaseObject.h"
#import <objc/runtime.h>
@implementation MyDatabaseObject {
@private
BOOL isImmutable;
#if MyDatabaseObjectTracksChangedProperties
NSMutableSet *changedProperties;
#endif
}
@synthesize isImmutable = isImmutable;
/**
* Make sure all your subclasses call this method ([super init]).
**/
- (instancetype)init
{
if ((self = [super init]))
{
#if MyDatabaseObjectTracksChangedProperties
// 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];
#endif
}
return self;
}
#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
{
// You can optionally call this method via [super copyWithZone:zone].
//
// But since this method only sets isImmutable to NO (the default value),
// doing so is optional.
id copy = [[[self class] alloc] init];
((MyDatabaseObject *)copy)->isImmutable = NO;
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
//{
// id copy = [self copy];
// ((MyDatabaseObject *)copy)->isImmutable = NO;
//
// return copy;
//}
#pragma mark Logic
- (void)makeImmutable
{
if (!isImmutable)
{
// Set immutable flag
isImmutable = YES;
#if !MyDatabaseObjectTracksChangedProperties
// 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];
#endif
}
}
+ (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 ...
// }
return [self immutableProperties];
}
+ (NSMutableSet *)immutableProperties
{
// This method returns a list of all properties that 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, if you have any 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 temporary.
//
// So your subclass would override this method like so:
//
// + (NSMutableSet *)immutableProperties
// {
// NSMutableSet *immutableProperties = [super immutableProperties];
// [immutableProperties removeObject:@"cachedTransformedAvatarImage"];
//
// return immutableProperties;
// }
NSMutableSet *dependencies = nil;
Class rootClass = [MyDatabaseObject class];
Class subClass = [self class];
while (subClass != rootClass)
{
unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList(subClass, &count);
if (properties)
{
if (dependencies == nil)
dependencies = [NSMutableSet setWithCapacity:count];
for (unsigned int i = 0; i < count; i++)
{
const char *name = property_getName(properties[i]);
NSString *property = [NSString stringWithUTF8String:name];
[dependencies addObject:property];
}
free(properties);
}
subClass = [subClass superclass];
}
return dependencies;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Nothing to do
}
- (void)willChangeValueForKey:(NSString *)key
{
if (isImmutable)
{
@throw [self immutableExceptionForKey:key];
}
[super willChangeValueForKey:key];
}
#if MyDatabaseObjectTracksChangedProperties
- (void)didChangeValueForKey:(NSString *)key
{
if (changedProperties == nil)
changedProperties = [[NSMutableSet alloc] init];
[changedProperties addObject:key];
[super didChangeValueForKey:key];
}
#endif
- (void)dealloc
{
#if MyDatabaseObjectTracksChangedProperties
[self removeObserver:self forKeyPath:@"isImmutable" context:NULL];
#else
if (isImmutable)
{
[self removeObserver:self forKeyPath:@"isImmutable" context:NULL];
}
#endif
}
- (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];
}
#if MyDatabaseObjectTracksChangedProperties
- (NSSet *)changedProperties
{
// We may have tracked changes to properties that are excluded from the list.
// For example, temp properties used for caching transformed values.
//
// @see [MyDatabaseObject immutableProperties]
//
[changedProperties unionSet:[[self class] immutableProperties]];
// And return immutable NSSet
return [changedProperties copy];
}
- (void)clearChangedProperties
{
changedProperties = nil;
}
#endif
@end