Saving PFObject NSCoding
Asked Answered
H

4

10

My Problem: saveInBackground isn't working.

The Reason It's not working: I'm saving PFObjects stored in an NSArray to file using NSKeyedArchiving. The way I do that is by implementing NSCoding via this library. For some reason unknown to me, several other fields are being added and are set to NULL. I have a feeling that this is screwing up the API call to saveInBackground. When I call saveInBackground on the first set of objects (before NSKeyedArchiving) saveInBackground works just fine. However, when I call it on the second object (after NSKeyedArchiving) it does not save. Why is this?

Save

[NSKeyedArchiver archiveRootObject:_myArray toFile:[self returnFilePathForType:@"myArray"]];

Retrieval

_myArray = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithFile:
                                             [self returnFilePathForType:@"myArray"]];

Object before NSArchiving

2014-04-16 16:34:56.267 myApp[339:60b]
<UserToMessage:bXHfPM8sDs:(null)> {
    from = "<PFUser:sdjfa;lfj>";
    messageText = "<MessageText:asdffafs>";
    read = 0;
    to = "<PFUser:asdfadfd>";
}
2014-04-16 16:34:56.841 myApp[339:60b]
<UserToMessage:bXHsdafdfs:(null)> {
    from = "<PFUser:eIasdffoF3gi>";
    messageText = "<MessageText:asdffafs>";
    read = 1;
    to = "<PFUser:63sdafdf5>";
}

Object after NSArchiving

<UserToMessage:92GGasdffVQLa:(null)> {
    ACL = "<null>";
    createdAt = "<null>";
    from = "<PFUser:eIQsadffF3gi>";
    localId = "<null>";
    messageText = "<MessageText:EudsaffdHpc>";
    objectId = "<null>";
    parseClassName = "<null>";
    read = 0;
    saveDelegate = "<null>";
    to = "<PFUser:63spasdfsxNp5>";
    updatedAt = "<null>";
}

2014-04-16 16:37:46.527 myApp[352:60b]
<UserToMessage:92GadfQLa:(null)> {
    ACL = "<null>";
    createdAt = "<null>";
    from = "<PFUser:eIQsadffF3gi>";
    localId = "<null>";
    messageText = "<MessageText:EuTndasHpc>";
    objectId = "<null>";
    parseClassName = "<null>";
    read = 1;
    saveDelegate = "<null>";
    to = "<PFUser:63spPsadffp5>";
    updatedAt = "<null>";
}

Update Using Florent's PFObject Category:

PFObject+MyPFObject_NSCoding.h

#import <Parse/Parse.h>

@interface PFObject (MyPFObject_NSCoding)

-(void) encodeWithCoder:(NSCoder *) encoder;
-(id) initWithCoder:(NSCoder *) aDecoder;
@end

@interface PFACL (extensions)
-(void) encodeWithCoder:(NSCoder *) encoder;
-(id) initWithCoder:(NSCoder *) aDecoder;
@end


 PFObject+MyPFObject_NSCoding.m

#import "PFObject+MyPFObject_NSCoding.h"
@implementation PFObject (MyPFObject_NSCoding)
#pragma mark - NSCoding compliance
#define kPFObjectAllKeys @"___PFObjectAllKeys"
#define kPFObjectClassName @"___PFObjectClassName"
#define kPFObjectObjectId @"___PFObjectId"
#define kPFACLPermissions @"permissionsById"
-(void) encodeWithCoder:(NSCoder *) encoder{

    // Encode first className, objectId and All Keys
    [encoder encodeObject:[self className] forKey:kPFObjectClassName];
    [encoder encodeObject:[self objectId] forKey:kPFObjectObjectId];
    [encoder encodeObject:[self allKeys] forKey:kPFObjectAllKeys];
    for (NSString * key in [self allKeys]) {
        [encoder  encodeObject:self[key] forKey:key];
    }


}
-(id) initWithCoder:(NSCoder *) aDecoder{

    // Decode the className and objectId
    NSString * aClassName  = [aDecoder decodeObjectForKey:kPFObjectClassName];
    NSString * anObjectId = [aDecoder decodeObjectForKey:kPFObjectObjectId];


    // Init the object
    self = [PFObject objectWithoutDataWithClassName:aClassName objectId:anObjectId];

    if (self) {
        NSArray * allKeys = [aDecoder decodeObjectForKey:kPFObjectAllKeys];
        for (NSString * key in allKeys) {
            id obj = [aDecoder decodeObjectForKey:key];
            if (obj) {
                self[key] = obj;
            }

        }
    }
    return self;
}
@end
Harrisonharrod answered 16/4, 2014 at 20:43 Comment(18)
So far I didn't used NSKeyedArchiving and see no point of it. Simply save your array to file and I guess saveinbackground doesn't support NSKeyedArchiving object. Anything comes up let me know.Quentin
@walle84 how would I save it to file without saving to background?Harrisonharrod
try saving without using NSKeyedArchiving and create array and try saveinbackground. Check this link might help you out.Quentin
Did you know you can save various object at the save time? [PFObject saveAllInBackground:<#(NSArray *)#>];Monique
@walle84 you're correct that NSKeyedArchiver is the problem, however I now need an alternative solution to save to disk.Harrisonharrod
@Monique that is not the problem.Harrisonharrod
@Auser cool so vote up !!! Me right ;) Now back... so you want to store it to local storage then you could look into Coredata else if parse then that link will help you out. Anything else then let me know.Quentin
@Auser u wana store in local storage den use CoreData else for parse u can check out these links link1 and link2 Also you should go through this document if you haven't.Quentin
@walle84 I want to implement my own custom cacheHarrisonharrod
If you have a huge data to cache then you must go for Coredata or Sqlite and if small then go for Nsuserdefault or plist file or file system. this division is as if you try to store large data using Nsuserdefault or file then it will take lot of time.Quentin
@walle84 yep I understand this. That's why I"m trying to use NSKeyedArchiver. Saving to a plist file won't work but PFObject's aren't compatibleHarrisonharrod
So I think what you could do is call save method to save object to parse.com then when success go for NskeyedArchiver. Also I'm don't have much idea about it. Just read about it and I think it's cool and easy way to save data . Thumb's UP !Quentin
Hey The field which are null ,these are filled by parse.com by it's own. So what you could do is save pfobject and when you fetch then you could get all detail's but this will be a long and i guess bad approach.Quentin
Can you explain why you want to implement your own custom cache? Maybe a different solution is what you need, rather than help with your current solution's errors. I.e. what do you need the cache for that the built-in Parse cache does not offer?Sporophyte
@Handsomeguy The reason I need my own custom cache as opposed to Parse's built-in cache is because I need access to the cache. I need to know what contents have been downloaded and saved to disk at all times, as well as make modifications to objects in the cache and save that data to the server. Storing my objects via NSArchiver or some other caching method is the simplest solution. I could implement Core Data but that would be overkill at this point.Harrisonharrod
I don't know if you've solved the null part yet, but the problem might be because those keys exist as properties on a PFObject, and they are filled automatically by parse when it interacts with the server. If you archive before populating these properties, they will be coded to NULL. Have you saved these objects to Parse and its still coding NULL?Orton
As a general rule... problems like this are why you should avoid third party libraries if at all possible. NSCoding is very simple, spend an hour learning how it works and do it yourself. It's rarely more than 10 or 20 lines of code.Quant
@AbhiBeckert that's great advice. Your comment also makes me think that this is an issue with the Parse SDK, not the NSCoding library, given that it's so simple to implement.Harrisonharrod
H
0

I have created a very simple workaround that requires no change the above NSCoding Libraries:

PFObject *tempRelationship = [PFObject objectWithoutDataWithClassName:@"relationship" objectId:messageRelationship.objectId];
        [tempRelationship setObject:[NSNumber numberWithBool:YES] forKey:@"read"];
        [tempRelationship saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            if (succeeded)
                NSLog(@"Success");
            else
                NSLog(@"Error");
        }];

What we're doing here is creating a temporary object with the same objectId, and saving it. This is a working solution that does not create a duplicate of the object on the server. Thanks to everyone who has helped out.

Harrisonharrod answered 22/5, 2014 at 2:6 Comment(1)
See also my comment aboveOh
O
3

The reason you are getting all the "<null>" entries after NSArchiving is because of the way the NSCoding library you used handles nil Parse properties. In particular, in a commit on 18th Feb, several changes occurred to the handling of nil, including removal of several tests to see if a property was nil plus addition of the following code inside the decode:

    //Deserialize each nil Parse property with NSNull
    //This is to prevent an NSInternalConsistencyException when trying to access them in the future
    for (NSString* key in [self dynamicProperties]) {
        if (![allKeys containsObject:key]) {
            self[key] = [NSNull null];
        }
    }

I suggest you use an alternative NSCoding library.

@AaronBrager suggested an alternative library in his answer on 22nd Apr.

UPDATED:

Since the alternative library is missing support for PFFile, below is a category implementation of the changes you need to implement NSCoding for PFFile. Simply compile and add PFFile+NSCoding.m to your project. This implementation is from the original NSCoding library you used.

PFFile+NSCoding.h

//
//  PFFile+NSCoding.h
//  UpdateZen
//
//  Created by Martin Rybak on 2/3/14.
//  Copyright (c) 2014 UpdateZen. All rights reserved.
//

#import <Parse/Parse.h>

@interface PFFile (NSCoding)

- (void)encodeWithCoder:(NSCoder*)encoder;
- (id)initWithCoder:(NSCoder*)aDecoder;

@end

PFFile+NSCoding.m

//
//  PFFile+NSCoding.m
//  UpdateZen
//
//  Created by Martin Rybak on 2/3/14.
//  Copyright (c) 2014 UpdateZen. All rights reserved.
//

#import "PFFile+NSCoding.h"
#import <objc/runtime.h>

#define kPFFileName @"_name"
#define kPFFileIvars @"ivars"
#define kPFFileData @"data"

@implementation PFFile (NSCoding)

- (void)encodeWithCoder:(NSCoder*)encoder
{
    [encoder encodeObject:self.name forKey:kPFFileName];
    [encoder encodeObject:[self ivars] forKey:kPFFileIvars];
    if (self.isDataAvailable) {
        [encoder encodeObject:[self getData] forKey:kPFFileData];
    }
}

- (id)initWithCoder:(NSCoder*)aDecoder
{
    NSString* name = [aDecoder decodeObjectForKey:kPFFileName];
    NSDictionary* ivars = [aDecoder decodeObjectForKey:kPFFileIvars];
    NSData* data = [aDecoder decodeObjectForKey:kPFFileData];

    self = [PFFile fileWithName:name data:data];
    if (self) {
        for (NSString* key in [ivars allKeys]) {
            [self setValue:ivars[key] forKey:key];
        }
    }
    return self;
}

- (NSDictionary *)ivars
{
    NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
    unsigned int outCount;

    Ivar* ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++){
        Ivar ivar = ivars[i];
        NSString* ivarNameString = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSValue* value = [self valueForKey:ivarNameString];
        if (value) {
            [dict setValue:value forKey:ivarNameString];
        }
    }

    free(ivars);
    return dict;
}

@end

SECOND UPDATE:

The updated solution I have described (using the combination of Florent's PFObject / PFACL encoders replacing className with parseClassName plus Martin Rybak's PFFile encoder) DOES work - in the test harness below (see code below) the second call to saveInBackground call does work after a restore from NSKeyedUnarchiver.

- (void)viewDidLoad {
    [super viewDidLoad];

    PFObject *testObject = [PFObject objectWithClassName:@"TestObject"];
    testObject[@"foo1"] = @"bar1";
    [testObject saveInBackground];

    BOOL success = [NSKeyedArchiver archiveRootObject:testObject toFile:[self returnFilePathForType:@"testObject"]];
    NSLog(@"Test object after archive (%@): %@", (success ? @"succeeded" : @"failed"), testObject);

    testObject = [NSKeyedUnarchiver unarchiveObjectWithFile:[self returnFilePathForType:@"testObject"]];
    NSLog(@"Test object after restore: %@", testObject);

    // Change the object
    testObject[@"foo1"] = @"bar2";
    [testObject saveInBackground];
}

- (NSString *)returnFilePathForType:(NSString *)param {
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *filePath = [docDir stringByAppendingPathComponent:[param stringByAppendingString:@".dat"]];

    return filePath;
}

However, looking at the Parse server, the second call to saveInBackground has created new version of the object.

Even though this is beyond the scope of the original question, I'll look to see if it is possible to encourage the Parse server to re-save the original object. Meanwhile please up vote and / or accept the answer given it solves the question of using saveInBackground after NSKeyedArchiving.

FINAL UPDATE:

This issue turned out to just be a timing issue - the first saveInBackground had not completed when the NSKeyedArchiver occurred - hence the objectId was still nil at the time of archiving and hence was still a new object at the time of the second saveInBackground. Using a block (similar to below) to detect when the save is complete and it is ok to call NSKeyedArchiver would also work

The following version does not cause a second copy to be saved:

- (void)viewDidLoad {
    [super viewDidLoad];

    __block PFObject *testObject = [PFObject objectWithClassName:@"TestObject"];
    testObject[@"foo1"] = @"bar1";
    [testObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (succeeded) {
            BOOL success = [NSKeyedArchiver archiveRootObject:testObject toFile:[self returnFilePathForType:@"testObject"]];
            NSLog(@"Test object after archive (%@): %@", (success ? @"succeeded" : @"failed"), testObject);

            testObject = [NSKeyedUnarchiver unarchiveObjectWithFile:[self returnFilePathForType:@"testObject"]];
            NSLog(@"Test object after restore: %@", testObject);

            // Change the object
            testObject[@"foo1"] = @"bar2";
            [testObject saveInBackground];
        }
    } ];

}
Oh answered 19/5, 2014 at 1:23 Comment(14)
thanks for the response. I tried implementing Florent's category, but it doesn't support PFFile archiving and I'm not an expert on iOS so I don't know how to write this myselfHarrisonharrod
I've updated the answer to provide an NSCoding implementation for PFFile that was present in the original NSCoding library you used. The licence for the original NSCoding library is here. It is the MIT licence and permits copying the source code provided the original licence notice is included.Oh
thanks for your update. Another issue I've run into is that when I implement Florent's category, I get the following error: No visible @interface for 'PFObject' declares the selector 'classsName'. Do you know how to resolve this?Harrisonharrod
Change className to parseClassName - i.e. The line [encoder encodeObject:[self className] forKey:kPFObjectClassName]; should read [encoder encodeObject:[self parseClassName] forKey:kPFObjectClassName];Oh
it still doesn't work, even when I implement Florent's PBObject NSCoding library while still using the PFFile coding I'm already using.Harrisonharrod
Please provide details.Oh
What errors are you getting at which line of code? Please include the code. Also you say coding I'm already using - please confirm that you do NOT have PFObject+NSCoding.m from your original library included in your project.Oh
Yes I removed it from the project when testing Florent's category. And I'm not getting any specific errors. It simply won't save when I try to saveinbackground using the Parse SDK. Also, the null fields are the same when using Florent's category.Harrisonharrod
I'll build a test harness to attempt to reproduce your problem. It might take a while.Oh
if you can figure out what the problem is, I would be incredibly happy and thankful.Harrisonharrod
@Oh the problem is the dirty properties of the copied object. If the object is not marked as dirty (or none of its children are) then the network request is skipped. To do the request you actually need to inform the object that some/all the keys of the object are dirty. Sadly enough Parse doesn't give a public API to do this.Mutualism
@AntonioE. - the dirty flags are not the issue - the decoder behaves no differently to the user writing their own update to a child of the object. Hence an object after a decode IS dirty, but according to the documentation, Parse does a clever save and detects which fields have changed value since the last save.Oh
@Oh I'll award you the bounty for all of the trouble you have gone through, but I believe the answer that I just posted achieves a better result with less changes. Thanks so much for your time and effort.Harrisonharrod
@Auser - thanks. BTW - I have debugged the test harness - the reason a second version was saved is only a timing issue - the first saveInBackground had not completed when the NSKeyedArchiver occurred - hence the objectId was still nil at the time of archiving and hence was still a new object at the time of the second saveInBackground. Using a block (similar to below) to detect when the save is complete and it is ok to call NSKeyedArchiver would also work. Cheers, Mike.Oh
D
0

PFObject doesn't implement NSCoding, and it looks like the library you're using isn't encoding the object properly, so your current approach won't work.

The approach recommended by Parse is to cache your PFQuery objects to disk by setting the cachePolicy property:

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
query.cachePolicy = kPFCachePolicyNetworkElseCache;
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    // Results were successfully found, looking first on the
    // network and then on disk.
  } else {
    // The network was inaccessible and we have no cached data for
    // this query.
  }
}];

(Code from the Caching Queries documentation.)

Then your app will load from the cache. Switch to kPFCachePolicyCacheElseNetwork if you want to try the disk cache first (faster, but possibly out of date.)

Your query object's maxCacheAge property sets how long something will stay on disk before it expires.


Alternatively, there's a PFObject category by Florent here that adds NSCoder support to PFObject. It's different than the implementation in the library you linked to, but I'm not sure how reliable it is. It may be worth experimenting with.

Dogbane answered 22/4, 2014 at 13:24 Comment(8)
did you read my question? I'm already implementing an NSCoding category github.com/updatezen/Parse-NSCodingHarrisonharrod
Yes, and it's not working… did you look at the different implementation I linked to at the end of my answer?Dogbane
Would I have to import "PFObjectNSCoding+PFObject.h" into any file that I want to have NSCoding support for?Harrisonharrod
could you go into more detail about how I would implement FLorent's category? I'm relatively new to Objective-C and iPhone development in general.Harrisonharrod
do you care to elaborate on your answer? This bounty expires in 2 days and I still have not received an adequate answer. Thanks.Harrisonharrod
Maybe you'd have better answers if you answered yourself our questions.Tehuantepec
@Auser "How to use categories" is outside the scope of this question, but see rypress.com/tutorials/objective-c/categories.html. It explains everything I would write here.Dogbane
@AaronBrager agreed, using categories is out of the scope of this question. I do have a last comment however: how would you extend Florent's NSCoding category to support PFFile archiving?Harrisonharrod
H
0

I have created a very simple workaround that requires no change the above NSCoding Libraries:

PFObject *tempRelationship = [PFObject objectWithoutDataWithClassName:@"relationship" objectId:messageRelationship.objectId];
        [tempRelationship setObject:[NSNumber numberWithBool:YES] forKey:@"read"];
        [tempRelationship saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            if (succeeded)
                NSLog(@"Success");
            else
                NSLog(@"Error");
        }];

What we're doing here is creating a temporary object with the same objectId, and saving it. This is a working solution that does not create a duplicate of the object on the server. Thanks to everyone who has helped out.

Harrisonharrod answered 22/5, 2014 at 2:6 Comment(1)
See also my comment aboveOh
T
-1

As you said in your question, the null fields must be screwing up the saveInBackground calls.

The weird thing is that the parseClassName is also null, while this must probably be required by Parse to save it. Is it set before you save your NSArray in the file ?

So I see two solutions :

  • implementing yourself NSCoding without the null fields, but if the object has already been saved on the server, it's useful (even necessary) to save its objectIds, createdAt, updatedAt fields, etc...
  • save each PFObject on Parse before saving your NSArray in a file, so those fields won't be null.
Tehuantepec answered 22/4, 2014 at 10:5 Comment(3)
I think you are on to something with your second bullet pointSewel
@Tehuantepec this is not possible. The objects need to be saveable even after the user has exited out of the current session (i.e. stored to disk)Harrisonharrod
@Auser Ok, have you tried to display the null fields before and after saving object on your file ? Maybe something is lost when it is archived.Tehuantepec

© 2022 - 2024 — McMap. All rights reserved.