问题
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
回答1:
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];
}
} ];
}
回答2:
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.
回答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.
回答4:
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 yourNSArray
in a file, so those fields won't be null.
来源:https://stackoverflow.com/questions/23119739/saving-pfobject-nscoding