Pergunta

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
Foi útil?

Solução 3

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.

Outras dicas

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];
        }
    } ];

}

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.

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.
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top