Сохранение PFObject NSCoding
Моя проблема: saveInBackground
не работает
Причина не работает: я экономлю PFObjects
хранится в NSArray в файл с помощью NSKeyedArchiving
, Я делаю это путем реализации NSCoding
через эту библиотеку. По какой-то неизвестной мне причине, несколько других полей добавляются и имеют значение NULL. У меня такое ощущение, что это портит вызов API saveInBackground
, Когда я звоню saveInBackground
на первом наборе объектов (до NSKeyedArchiving
) saveInBackground
работает просто отлично. Однако, когда я вызываю его на втором объекте (после NSKeyedArchiving
) это не спасает. Почему это?
Сохранить
[NSKeyedArchiver archiveRootObject:_myArray toFile:[self returnFilePathForType:@"myArray"]];
поиск
_myArray = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithFile:
[self returnFilePathForType:@"myArray"]];
Объект перед 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>";
}
Объект после 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>";
}
Обновление с использованием категории Florent PFObject:
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
4 ответа
Я создал очень простой обходной путь, который не требует изменений выше NSCoding
Библиотеки:
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");
}];
Здесь мы создаем временный объект с тем же objectId и сохраняем его. Это рабочее решение, которое не создает дубликат объекта на сервере. Спасибо всем, кто выручил.
Причина, по которой вы получаете все "<null>"
Записи после NSArchiving объясняются тем, как используемая вами библиотека NSCoding обрабатывает свойства nil Parse. В частности, в коммите 18 февраля произошли некоторые изменения в обработке nil, включая удаление нескольких тестов, чтобы увидеть, было ли свойство nil, плюс добавление следующего кода внутри декодера:
//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];
}
}
Я предлагаю вам использовать альтернативную библиотеку NSCoding.
@AaronBrager предложил альтернативную библиотеку в своем ответе 22 апреля.
ОБНОВЛЕНО:
Поскольку в альтернативной библиотеке отсутствует поддержка PFFile, ниже приведена категория реализации изменений, необходимых для реализации NSCoding для PFFile. Просто скомпилируйте и добавьте PFFile+NSCoding.m
к вашему проекту. Эта реализация взята из оригинальной библиотеки NSCoding, которую вы использовали.
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
ВТОРОЕ ОБНОВЛЕНИЕ:
Обновленное решение, которое я описал (с заменой кодировщиков PFObject / PFACL от Florent) className
с parseClassName
плюс кодер PFFile Мартина Рыбака) РАБОТАЕТ - в тестовом жгуте ниже (см. код ниже) второй вызов saveInBackground
вызов работает после восстановления из 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;
}
Однако, глядя на сервер Parse, второй вызов saveInBackground
создал новую версию объекта.
Несмотря на то, что это выходит за рамки исходного вопроса, я посмотрю, можно ли побудить сервер Parse повторно сохранить исходный объект. Тем временем, пожалуйста, проголосуйте и / или примите ответ, если он решает вопрос об использовании saveInBackground
после NSKeyedArchiving
,
ЗАКЛЮЧИТЕЛЬНОЕ ОБНОВЛЕНИЕ:
Эта проблема оказалась просто проблемой синхронизации - первый saveInBackground не завершился, когда произошел NSKeyedArchiver - следовательно, objectId был все еще нулевым во время архивирования и, следовательно, все еще был новым объектом во время второго saveInBackground. Использование блока (аналогично приведенному ниже) для определения того, когда сохранение завершено, и можно вызывать NSKeyedArchiver, также будет работать
Следующая версия не приводит к сохранению второй копии:
- (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
не реализует NSCoding
и похоже, что используемая вами библиотека неправильно кодирует объект, поэтому ваш текущий подход не сработает.
Подход, рекомендуемый Parse, заключается в кешировании PFQuery
объекты на диск, установив cachePolicy
имущество:
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.
}
}];
(Код из документации кеширующих запросов.)
Тогда ваше приложение загрузится из кеша. Переключиться на kPFCachePolicyCacheElseNetwork
если вы хотите сначала попробовать кеш диска (быстрее, но, возможно, устарело)
Ваш запрос объекта maxCacheAge
свойство устанавливает, как долго что-то будет оставаться на диске до истечения срока его действия.
Кроме того, здесь есть категория PFObject от Florent, которая добавляет поддержку NSCoder в PFObject. Это отличается от реализации в библиотеке, с которой вы связаны, но я не уверен, насколько она надежна. Возможно, стоит поэкспериментировать с.
Как вы сказали в своем вопросе, нулевые поля должны испортить saveInBackground
звонки.
Странно то, что parseClassName также имеет значение null, хотя Parse, вероятно, должен его сохранять, чтобы сохранить. Это установлено до того, как вы сохраните NSArray
в файле?
Итак, я вижу два решения:
- реализовать себя
NSCoding
без полей NULL, но если объект уже был сохранен на сервере, полезно (даже необходимо) сохранить его поля objectIds, creationAt, updatedAt и т. д. - сохранить каждый
PFObject
на Parse перед сохранениемNSArray
в файле, поэтому эти поля не будут нулевыми.