FatFractal customer forums



Author Topic: Getting objects calls default initializer  (Read 2781 times)

josgrant

  • Newbie
  • *
  • Posts: 6
    • View Profile
Getting objects calls default initializer
« on: January 15, 2014, 04:33:55 PM »
I'm interested in pulling data from FatFractal and persisting the objects in CoreData. I'm doing this to allow an "offline mode" where client-side calculations will still work with the data I've persisted. The problem I'm running into is FatFractal's auto-initialization (if that's even a thing). When I try to get an object / object array from FatFractal, it figures out which object I'm creating and tries to initialize it. However, CoreData's default initializer is a little different so my app crashes on a pull. What I would like to do would be to pull all of the object's data and then decide what I would like to do with it.

I've tried [FatFractal main].autoLoadsRefs = NO; which doesn't fix the crash.

If there's some secret to changing the initialization, let me know!

josgrant

  • Newbie
  • *
  • Posts: 6
    • View Profile
Re: Getting objects calls default initializer
« Reply #1 on: January 16, 2014, 12:30:28 PM »
So I'm thinking of creating wrapper classes for all of my core data objects (same properties) and having FatFractal initialize those when it syncs. Then I'm going to determine which objects were updated / added / deleted based on a timestamp that I'll have each one get when it changes its value. Then, based on the modification, I'll update my managed object context and then persist it. It'll be a bit of work but I shouldn't have to do it more than once for each entity.

However, this isn't an optimal solution. Replies are still welcome!

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #2 on: January 16, 2014, 01:05:41 PM »
Hi there,

We don't directly support CoreData right now; it's been on the list for ages but nobody's shouted loud enough yet :-) From what I remember when I last checked, it looked relatively straightforward (basically, provide a way to hook in entities and MOCs for classes so that the deserializer can call initWithEntity:insertIntoManagedObjectContext: ) ... if this is a show-stopper for you, I'll get it done quickly.

I should mention - as of last weekend's release, the iOS SDK now provides client-side caching. We did it this way because it is consistent across both our Android and IOS SDKs. I am preparing a blog post about this feature, but I was ill for a couple of days and am still playing catch-up. However it's pretty straightforward; here's a test harness which
  • sets up local storage
  • logs in
  • creates some objects
  • Retrieves some objects using a query, & store to cache
  • simulates being offline
  • Issue the query but don't ask to use the cache - should be an error
  • Issue the same query again, asking to use the cache if offline - should work fine

Code: [Select]
#import <SenTestingKit/SenTestingKit.h>
#import <FFEF/FatFractal.h>

static FatFractal * ff;
static id<FFLocalStorage> localStorage;

@interface Basic : NSObject
@property (nonatomic) int anInt;
@end

@implementation Basic
@end

@interface BasicCachingTest : SenTestCase

@end

@implementation BasicCachingTest

- (void)setUp
{
    [super setUp];
   
    if (! ff) {
        ff = [[FatFractal alloc] initWithBaseUrl:@"https://localhost:8443/TestApp"];
        // Do any calls to registerClass:ForClazz: here, BEFORE setting the localStorage property
        // This is essential when one is subclassing FFUser for example

        localStorage = [[FFLocalStorageSQLite alloc] initWithDatabaseKey:@"CachingTests"];
        [localStorage setDebug:NO];
        [localStorage wipeAllData];
       
        ff.localStorage = localStorage;
    }
    ff.debug = NO;
    ff.simulatingOffline = NO;
}

- (void)tearDown
{
    [super tearDown];
}

- (void) pause:(NSTimeInterval)pauseInterval
{
    NSDate* cycle = [NSDate dateWithTimeIntervalSinceNow:pauseInterval];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                             beforeDate:cycle];
}

- (NSError *) deleteObjsForQuery:(NSString *)query {
    NSError *crudError;
   
    NSArray * objs = [ff getArrayFromUri:query error:&crudError];
   
    if (! crudError) {
        for (id obj in objs) {
            crudError = nil;
            [ff deleteObj:obj error:&crudError];
            STAssertNil(crudError, @"deleteObjectsForQuery %@ : Failed to delete %@", query, obj);
            if (crudError)
                break;
        }
    }
    return crudError;
}

- (void) testBasicCreateAndRetrieve
{
    // We're going to run this all synchronously but use the block methods which would be used in normal code
    __block BOOL blockComplete = NO;
    __block NSError *blockErr;
   
    // Login
    NSLog(@"***testBasicCreateAndRetrieve: logging in");
    [ff loginWithUserName:@"TestUser_1" andPassword:@"TestUser_1" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
        STAssertNil(theErr, @"Failed to login - error was: %@", [theErr localizedDescription]);
        blockErr = theErr;
        blockComplete = YES;
    }];
    while (!blockComplete) [self pause:0.001];
    if (blockErr) return;
   
    // Delete existing test objects
    NSLog(@"***testBasicCreateAndRetrieve: deleting old test objects");
    NSError *deleteErr = [self deleteObjsForQuery:@"/Basic"];
    STAssertNil(deleteErr, @"Failed to delete existing objects");
    if (deleteErr) return;
   
    // Create 5 objects
    NSLog(@"***testBasicCreateAndRetrieve: creating new test objects");
    for (int i = 1; i <= 5; i++) {
        blockErr = nil;
        blockComplete = NO;
        Basic * basic = [[Basic alloc] init]; basic.anInt = i;
        [ff createObj:basic atUri:@"/Basic" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
            blockErr = theErr;
            STAssertNil(blockErr, @"Failed to create object - error was: %@", [blockErr localizedDescription]);
            blockComplete = YES;
        }];
        while (!blockComplete) [self pause:0.001];
        if (blockErr) return;
    }
   
    // Retrieve 2 of the objects via a query, and ask for the response to be cached
    NSLog(@"***testBasicCreateAndRetrieve: retrieving objects - caching response");
    blockErr = nil;
    blockComplete = NO;
    [[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithOptions:FFReadOptionCacheResponse andBlock:^(FFReadResponse *response)
     {
         blockErr = [response error];
         STAssertNil(blockErr, @"Failed to retrieve objects - error was: %@", [blockErr localizedDescription]);
         blockComplete = YES;
     }];
    while (!blockComplete) [self pause:0.001];
    if (blockErr) return;
   
    // Simulate being offline
    NSLog(@"***testBasicCreateAndRetrieve: simulating offline");
    [ff setSimulatingOffline:YES];
   
    // Issue the query again but don't ask to use the cache - should be an error
    NSLog(@"***testBasicCreateAndRetrieve: retrieving objects while offline - NOT USING cache - should get an error response");
    blockErr = nil;
    blockComplete = NO;
    [[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithBlock:^(FFReadResponse *response)
     {
         blockErr = [response error];
         STAssertNotNil(blockErr, @"Should have failed to retrieve objects");
         blockComplete = YES;
     }];
    while (!blockComplete) [self pause:0.001];
    if (! blockErr) return;
   
    // Issue the same query again, asking to use the cache if offline - should work fine
    NSLog(@"***testBasicCreateAndRetrieve: retrieving objects while offline - USING cache");
    blockErr = nil;
    blockComplete = NO;
    [[[ff newReadRequest] prepareGetFromUri:@"/Basic/(anInt between 2 and 3)"] executeAsyncWithOptions:FFReadOptionUseCachedIfOffline andBlock:^(FFReadResponse *response)
     {
         blockErr = [response error];
         STAssertNil(blockErr, @"Failed to retrieve objects from cache - error was: %@", [blockErr localizedDescription]);
         blockComplete = YES;
     }];
    while (!blockComplete) [self pause:0.001];
    if (blockErr) return;
}

@end

FYI, the various read options which you can use (and of course you can use some of them together) are:
Code: [Select]
typedef NS_OPTIONS(NSInteger, FFReadOption) {
    FFReadOptionAutoLoadRefs        = (0x1 << 0), // When retrieving objects, automatically retrieve objects which they reference
    FFReadOptionAutoLoadBlobs       = (0x1 << 1), // When retrieving objects, automatically retrieve any BLOBs they contain
    FFReadOptionCacheResponse       = (0x1 << 2), // When retrieving, cache the response
    FFReadOptionUseCachedOnly       = (0x1 << 3), // When retrieving, ONLY try the cache - i.e. do not hit the network
    FFReadOptionUseCachedIfCached   = (0x1 << 4), // When retrieving, try the cache first - only hit the network if cache is empty
    FFReadOptionUseCachedIfOffline  = (0x1 << 5)  // When retrieving, try the network first - if offline, then try the cache
};

Personally I tend to use FFReadOptionCacheResponse | FFReadOptionUseCachedIfOffline for all of the queries that I wish to have available while offline.

One nice additional benefit - when using localStorage like this, the logged-in user is also cached - so if for example you log in and then the app is terminated, then, when the app restarts and the FatFractal object is created again, it will restore the logged-in user, and loggedIn will be true.

Regards,

- Gary

josgrant

  • Newbie
  • *
  • Posts: 6
    • View Profile
Re: Getting objects calls default initializer
« Reply #3 on: January 16, 2014, 01:39:18 PM »
Hey Gary,

Thanks for the reply! I'd like to see how CoreData integration will work. Essentially, I don't know how CRUD operations and validation can function with managed objects, because as far as I know, I'd have to manage the objects' states to make sure there aren't duplicates. If I sync automatically and initWithEntityDescription:... is called, it will insert it into the managed object context and I would have to go in and remove them from the context. However, if there was some way to automagically manage all of the object states, that would be excellent and I would use it! Otherwise, I'm currently trying to implement a workaround that might give me the flexibility I need.

I'm hesitant to use your implementation of client-side caching because I don't know if it uses versioning and if it will work with multiple threads. Also, I'm wondering how efficient it is? Fetching objects and querying especially. I know it uses SQLite but I don't know what your wrappers do.

Sorry for my concerns! I like what I've seen from FatFractal thus far and I'm investing my time in it because I think it will work. I'm just an edge case for FF, so don't worry too much about it.

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #4 on: January 16, 2014, 01:52:39 PM »
With regards to caching - it is purely an offline cache (no versions) - works fine with multiple threads - basically it's very simple, we store what is returned from the server into a table where the key is the full query url. (eg https://yourdomain.fatfractal.com/yourApp/ff/resources/SomeCollection/(foo eq 'bar') - i.e. the data is stored as an opaque chunk of data.

It's definitely nothing like CoreData :-) I only mentioned it here because you mentioned offline operation.

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #5 on: January 16, 2014, 04:14:57 PM »
Hi there - I've just built a version of the iOS SDK which allows you to control the creation of new instances of your objects.

To use, you would subclass FatFractal and implement this method like so:
Code: [Select]
- (id) createInstanceOfClass:(Class) class {
    if ([NSStringFromClass(class) isEqualToString:@"MyCoreDataClass"]) {
        return [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(class)
                                             inManagedObjectContext:[DBManager context]];
    } else {
        return [[class alloc] init]; // This is essential, otherwise bad bad stuff will happen
    }
}

If this helps, let me know and I'll ship it to you.

- Gary

UPDATE: Feb 14: The version of the iOS SDK with this included was released on Feb 14

UPDATE: Feb 16: A new version of the iOS SDK in which you can implement this method
Code: [Select]
- (id) createInstanceOfClass:(Class) class forObjectWithMetaData:(FFMetaData *)objMetaData
is going through QA at the moment and will be released soon. You will probably need this method so that, given the FFMetaData, you can do the appropriate CoreData lookup to see if you already have a copy of it in CoreData locally.

UPDATE: March 1: New version of the SDK released. Get it from http://fatfractal.com/support/downloads/ - see also this sample project on GitHub https://github.com/FatFractal/fatfractal-code-samples/tree/master/TaggedLocations
« Last Edit: March 01, 2014, 12:57:50 PM by gkc »

josgrant

  • Newbie
  • *
  • Posts: 6
    • View Profile
Re: Getting objects calls default initializer
« Reply #6 on: January 16, 2014, 06:13:43 PM »
Hey Gary,

So I've implemented this method as such:

Code: [Select]
- (id)createInstanceOfClass:(Class)class
{
    if ([[[class superclass] description] isEqualToString:@"SyncedEntity"]) {
        return [NSEntityDescription insertNewObjectForEntityForName:[class description]
                                             inManagedObjectContext:[DBManager context]];
    } else {
        return [[class alloc] init];
    }
}

However, I'm running into a problem. It seems that there is a "EXC_BAD_ACCESS" error in
Code: [Select]
-[FatFractal setValuesOnObject:fromDict:loadCacheOnly:doAutoLoadRefs:doAutoLoadBlobs:]
which seems to get called when a
Code: [Select]
_NSFaultingMutableSetobject tries to pass through that initialization method. At this point, I'm assuming that there is some magic / voodoo that I don't have control over that's causing the problem, but it could be me! However, the User objects that I pull initialize with CoreData correctly, so I'm pleased that that is working.

Thanks Gary, I really appreciate how much you're helping with this!

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #7 on: January 16, 2014, 07:11:05 PM »
Hi Josh,

Have you tried with the second version of the SDK that I sent to you?

Also - can you please share
(1) The .h of the class which is triggering the problem
(2) The ObjectType definition on the FF backend

Cheers,

- Gary

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #8 on: January 16, 2014, 07:13:15 PM »
Also - before you make the call which triggers the crash, can you set ff.debug = YES and send me the output, should help identify how far it's getting in setting the values

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #9 on: January 16, 2014, 08:06:09 PM »
Hi Josh,

I'm pretty sure I know where the problem is and it will be straightforward to fix, but I'll know for sure when you send me your .h and the OBJECTTYPE definition and the debug output. But it's after my bedtime so I'm going to turn in now and come back to it tomorrow.

In the meantime, as a quick hack, you can override that method in your FatFractal subclass like this:
Code: [Select]
- (void) setValuesOnObject:(id)obj fromDict:(NSDictionary *)dict loadFromCacheOnly:(BOOL)loadFromCacheOnly
            doAutoLoadRefs:(BOOL)doAutoLoadRefs doAutoLoadBlobs:(BOOL)doAutoLoadBlobs
{
    if (obj is something I'm having to handle specially) {
        // set the values on the object from the dictionary
    } else {
        [super setValuesOnObject:obj fromDict:dict loadFromCacheOnly:loadFromCacheOnly doAutoLoadRefs:doAutoLoadRefs doAutoLoadBlobs:doAutoLoadBlobs];
    }
}


Regards,

- Gary

josgrant

  • Newbie
  • *
  • Posts: 6
    • View Profile
Re: Getting objects calls default initializer
« Reply #10 on: January 21, 2014, 12:39:58 PM »
Alright,

So everything is looking good with CoreData! I've successfully initialized the CoreData objects by creating a subclass of FatFractal and overriding the createClass method. Also, for each object that had references to other objects, i.e grab bags, I had to override the serialize method.

Gary's support is awesome. With all of his help, I was able to get everything working with CoreData and syncing. I had to write my own sync method that managed all of the CRUD operations, but it was only around 30 lines of code so not a big deal.


gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: Getting objects calls default initializer
« Reply #11 on: January 22, 2014, 02:47:02 PM »
Woo hoo! Glad it works. Will polish it up and put out in next dot release

mobiledjapps

  • Newbie
  • *
  • Posts: 3
    • View Profile
Re: Getting objects calls default initializer
« Reply #12 on: January 23, 2014, 11:33:11 AM »
I am having hard time gettting it to work.
So I created a subclass of the FatFractal:

https://gist.github.com/DJMobileInc/8581454 (.m and .h)

than I used it to create a local instance of the FatFractal:


Code: [Select]
        if(!_ff){
            _ff = [[MyFractal alloc] initWithBaseUrl:baseUrl];
            [_ff loginWithUserName:@"xx" andPassword:@"xx"];
            _ff.debug = YES;
        }
 
But I am still getting the core data errors:
2014-01-23 10:14:19.221 Fractio[17592:70b] getClassFromClazz found className (null) for clazz MFStudent
2014-01-23 10:14:19.222 Fractio[17592:70b] getClassFromClazz:MFStudent returning MFStudent
2014-01-23 10:14:19.222 Fractio[17592:70b] clazzToClassDict is {
}
2014-01-23 10:14:19.222 Fractio[17592:70b] CoreData: error: Failed to call designated initializer on NSManagedObject class 'MFStudent'
2014-01-23 10:14:19.223 Fractio[17592:70b] Trying to set value  (type __NSCFString) for key lastname (propAttributes T@"NSString",&,D,N)

And none of the methods in my FatFractal's subclass is called...

How should I proceed....?


kwylez

  • Newbie
  • *
  • Posts: 27
    • View Profile
Re: Getting objects calls default initializer
« Reply #13 on: January 23, 2014, 03:49:01 PM »
Did you call the parent designated initWithBaseUrl in your subclass?

kwylez

  • Newbie
  • *
  • Posts: 27
    • View Profile
Re: Getting objects calls default initializer
« Reply #14 on: January 23, 2014, 03:54:22 PM »
I've used CoreData with FF, but took a different approach. It is a little bit more "client" side setup, but IMHO keeps your model clean.

* As an aside, using the approach that Gary outlined (LocalStorage) keeps your data layer homogenous and don't inject another, though useful, technology like CoreData into the mix.

The most common approach for using CoreData is to have a 1:1 representation of your server model in your application. That _usually_ works well for most setups, but is almost impossible to do with FF. The reason being is due how FF handles "related data". BackReferences, Grabbags, References, etc. Trying to replicate that in CoreData is a fools errand, but the good news is you don't have to IF you use CoreData to only persist your necessary objects.

In one of the apps that I'm working on I have the following Object:

Code: [Select]
CREATE OBJECTTYPE Event (name STRING, attendCode STRING, location REFERENCE /Venues, attendees REFERENCE /FFUserGroup, startTime DATE, endTime DATE, playlist GRABBAG /Tracks, currentlyPlaying REFERENCE /Tracks, isPrivate BOOLEAN, photos GRABBAG /UserPhotos, ownerProfile REFERENCE /UserProfiles)
As you can see I've got references, grabbags and standard objecttypes. Trying to replicate this object graph with CoreData would be nightmare, especially given that I just wanted to persist the latest events. As a general rule my objects conform to NSSecureCoding so that I can persist them to anything I want. My solution that I came up with is to have an additional model for those objects that I wanted to persist to CoreData.

For example:

Event.h/m
  - name
  - attendCode
  - startTime
  - endTime
  - ....

Venues.h/m
.
.
.

CDEvent.h/m
 - created
 - event
 - ffguid
 - isPublic
 - updated
 - isCurrent

 All of the properties for the CDEvent object are the properties that I would want to display or query on thus why they are explicitly defined. The event property is the serialized instance itself. CoreData will automagically apply an NSValueTransform on the object (You do have to set the attribute type to Transformable, but you don't have to explicitly set the type of transform).

 I see a few benefits of taking this approach.
 
 1) My underlying data model remains "clean". I'm still dealing with straight NSObject's and don't have to jump through any hoops or hurdles going back/forth with FF.
 2) If I decide that I don't want to deal with CoreData anymore I don't have to rewrite my model and I can move to another persistance mechanism (sqlite, plist, seralized data on disk, memory, etc.)
 3) Allows me to mix and match persistance mechanisms. CoreData isn't a silver bullet for offline and sometimes it feels like using an atom bomb to light a grill.

 



 

Copyright © FatFractal customer forums