Сохранение 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

Ответ 1

Я создал очень простой обходной путь, который не требует изменений выше 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 и сохранение его. Это рабочее решение, которое не создает дубликат объекта на сервере. Спасибо всем, кто помог.

Ответ 2

Причина, по которой вы получаете все записи "<null>" после NSArchiving, объясняется тем, как используемая вами библиотека NSCoding обрабатывает свойства nil Parse. В частности, в фиксации 18 февраля произошло несколько изменений в обработке 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

ВТОРОЕ ОБНОВЛЕНИЕ:

Обновленное решение, которое я описал (используя комбинацию кодов Florent PFObject/PFACL, заменяя className на parseClassName plus Martin Rybak PFFile encoder) Работает - в тестовом жгуте ниже (см. код ниже) второй вызов 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];
        }
    } ];

}

Ответ 3

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. Это отличается от реализации в библиотеке, к которой вы привязались, но я не уверен, насколько она надежна. Возможно, стоит поэкспериментировать.

Ответ 4

Как вы сказали в своем вопросе, нулевые поля должны закручивать вызовы saveInBackground.

Странно то, что parseClassName также равно null, в то время как это, вероятно, потребуется Parse для его сохранения. Установлен ли он перед сохранением NSArray в файле?

Итак, я вижу два решения:

  • реализовать себя NSCoding без нулевых полей, но если объект уже сохранен на сервере, он полезен (даже необходимо) для сохранения его объектов, созданныхAt, updatedAt полей и т.д.
  • сохраните каждый PFObject в Parse перед сохранением NSArray в файле, чтобы эти поля не были пустыми.