tciuro / NanoStore

NanoStore is an open source, lightweight schema-less local key-value document store written in Objective-C for Mac OS X and iOS.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Saving of custom types inside a NSFNanoObject

erikvdwal opened this issue · comments

First things first: Thank you so much for creating NanoStore! It's really a joy to use.

There is one "problem" I'm currently running in to though, which resulted in this feature request (which may not even be legit).

At the moment, NanoStore allows the saving of types NSArray, NSDictionary, NSString, NSDate, NSNumber and NSData. This is enough in a lot of cases, but not really optimal in my case.

Consider the following scenario (disregarding the fact that in the real world, an author would probably have multiple posts):

@interface Post : NSFNanoObject

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *body;
@property (nonatomic, copy) NSArray *tags;
@property (nonatomic, copy) Author *author;
//@property (nonatomic, copy) NSArray *authors; // alternatively/additionally 

@end

@interface Author : NSObject <NSCoding>

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy) NSString *email;

- (NSString *)fullName;

@end

The author object has a convenience method fullName and maybe a few other methods. Right now, the type Author cannot be saved as a property of Post, so instead, I'm copying the author's details to an NSDictionary, and mapping them back to an Author object so I can access the convenience methods again.

I'm doing this multiple times for different objects and in different controllers, so I was wondering if it's also possible to add support for other types as well, so they can be 'nested' inside a NSFNanoObject in some way, so we'd have access to the convenience methods directly after fetching.

So something like this would work:

Post *post = [posts lastPost]; // some post
NSString *fullName = [post.author fullName]; // post.author is of type Author, not NSDictionary

Thanks again.

Hi Erik,

You're welcome :-)

The issue is that for custom classes, it's not obvious what to store. Some properties should be saved, while others are transient and should be ignored. Ultimately, it's the developer that knows what to save. I'm not sure why you're doing this multiple times in different controllers.

Using NSFNanoObjectProtocol you should be able to simplify things quite a bit in the place where it matters most: the data class. The methods - (NSDictionary *)nanoObjectDictionaryRepresentation and - (id)initNanoObjectFromDictionaryRepresentation:(NSDictionary *)theDictionary forKey:(NSString *)aKey store:(NSFNanoStore *)theStore are responsible for returning the dictionary equivalent of the data that should be serialized. Following your example we could do the this (some code removed for brevity):

@implementation Author

- (id)initNanoObjectFromDictionaryRepresentation:(NSDictionary *)theDictionary forKey:(NSString *)aKey store:(NSFNanoStore *)theStore
{
    self = [super init];

    if (self) {
        _firstName = theDictionary[@"firstName"];
         _lastName = theDictionary[@"lastName"];
        _email = theDictionary[@"email"];

         // Honor the key first. If not present, try to get it from the dictionary
        _key = aKey;
        if (!_key) {
            _key = theDictionary[@"key"];
         }
    }

    return self;
}

- (NSDictionary *)nanoObjectDictionaryRepresentation
{
    return @{ @"firstName" : self.firstName,
                @"lastName"  : self.lastName,
                @"email"     : self.email,
                @"key"       : self.key
              };
}
...
@end

@implementation Post

- (id)initNanoObjectFromDictionaryRepresentation:(NSDictionary *)theDictionary forKey:(NSString *)aKey store:(NSFNanoStore *)theStore
{
    self = [super init];

    if (self) {
        _title = theDictionary[@"title"];
         _body = theDictionary[@"body"];
        _tags = theDictionary[@"tags"];
        _key = aKey;

         // Instantiate the author
         NSDictionary *authorInfo = theDictionary[@"author"];
         if (authorInfo) {
             Class authorClass = NSClassFromString(@"Author")
            _author = [[authorClass alloc]initNanoObjectFromDictionaryRepresentation:authorInfo forKey:nil store:nil];
        }
    }

    return self;
}

- (NSDictionary *)nanoObjectDictionaryRepresentation
{
    return @{ @"title"  : self.title,
                @"body"   : self.body,
                @"tags"   : self.tags,
                @"author" : self.author.nanoObjectDictionaryRepresentation
              };
}
...
@end

You get to decide how to serialize and deserialize the data in one spot. Alternatively, you could create a convenience method in Author to reduce the code in Post:
@implementation Author

+ (Author *)authorWithInfo:(NSDictionary *)info
{
    return [[Author alloc]initNanoObjectFromDictionaryRepresentation:info forKey:nil store:nil];
}
...
@end

Now you could implement Post like this:
- (id)initNanoObjectFromDictionaryRepresentation:(NSDictionary *)theDictionary forKey:(NSString *)aKey store:(NSFNanoStore *)theStore
{
    self = [super init];

    if (self) {
        _title = theDictionary[@"title"];
         _body = theDictionary[@"body"];
        _tags = theDictionary[@"tags"];
        _key = aKey;

         // Instantiate the author
         _author = [[Author authorWithInfo:theDictionary[@"author"]];
    }

    return self;
}

There's some code involved, but it's all packed in one place, it's easy to find and can you can enhance it easily. Instantiating the "root" object (i.e. Post) should take care of deserializing its custom objects (and the custom objects specified within those.) In the end, you'll be able to do what you seek:
Post *post = [posts lastPost]; // some post
NSString *fullName = [post.author fullName];

I think this would work. What do you think?

Thanks for writing such an extensive answer.

This is more or less what I'm doing right now, just in the wrong place (the controller). It indeed makes sense to move everything to the data-class. Although I haven't had the opportunity to implement it, I think the solution you provided will work just fine. Thanks again!