Parse: Using PFObject subclasses with PFCloud calls - ios

I have a PFObject subclass called MediaFile. In order to get back instances of my subclass from calls to the server, I've seen how you can construct queries out of Parse subclasses like this:
PFQuery *query = [MediaFile query];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) { ... }];
However, most of the server calls in my app are custom Cloud calls, which return PFObjects. As soon as I've retrieved these objects I want to treat them as MediaFile objects and call custom methods on them that are defined in the MediaFile class. Casting does not work, because it does not actually construct a MediaFile object. What I've done to overcome this problem is construct new MediaFiles for each PFObject I get back using [MediaFile object], and then copy all the data into each one using a method I wrote called loadFromObject::
[PFCloud callFunctionInBackground:#"func" withParameters:#{} block:^(id objects, NSError *error) {
for (PFObject *object in objects) {
MediaFile *mf = [[MediaFile object] loadFromObject:object];
[array addObject:mf];
}
}];
In MediaFile.m:
- (MediaFile *) loadFromObject:(PFObject *)object {
NSArray *keys = [object allKeys];
for (NSString *key in keys) {
id obj = [object objectForKey:key];
[self setObject:obj forKey:key];
}
return self;
}
This works, but is there a more elegant way to address this problem?

How does your custom class defined?
Because their subclassing mechanism is relying on Key-Value-Coding(KVC).
Here's KVC Programming Guide. Or you can check out the guide over here.
The following sample should work.
In the header:
#interface CustomObject : PFObject <PFSubclassing>
#property (nonatomic, strong) NSString *propertyName;
In the implementation:
#import <Parse/PFObject+Subclass.h>
#pragma mark - columns of Parse table
#dynamic propertyName;
#pragma mark - regiester parse subclass in runtime
+ (void)load {
#autoreleasepool {
[self registerSubclass];
}
}
#pragma mark - parse cloud table name
+ (NSString *)parseClassName
{
return #"CustomObject"; // table name
}

TL;DR:
Make sure your subclass of PFObject is registered with Parse by the time your Cloud code returns those objects from a net call.
If you've subclassed PFObject using the instructions Parse includes in it's documentation guide, you can just make the following call before the first net call that returns your custom subclass:
CustomSubclass.initialize()
A good place to add this code is in your app delegate's application(didFinishLaunchingWithOptions:) method, where it will be likely to run before any other code related to your subclass.
Long version with explanation
My situation was a little different from yours, but I believe the problem is largely the same. My problem was that I was fetching objects through a PFQuery and the objects returned in the query callback were just generic PFObjects.
Here's what my code looked like:
// Scene is a PFObject subclass
func getLatestScenes(completion: ((scenes: [Scene]) -> Void)?) {
var query = PFQuery(className: SceneClassName)
query.findObjectsInBackgroundWithBlock { (results: [AnyObject]?, error: NSError?) -> Void in
if let scenes = results as? [Scene] {
// This would never run, as the members of `results` were only PFObjects
completion?(scenes: scenes)
} else {
// Code would always skip to the empty result case
completion?(scenes: [])
}
}
}
Individual members of the results array would print out in the debugger with descriptions that clearly stated that they were supposed to be of the class Scene, as intended, but when inspected they were only PFObjects.
(lldb) po results![0]
<Scene: 0x7fc743a8f310, objectId: YyxYH9dtBp, localId: (null)> {
creator = "<PFUser: 0x7fc74144b320, objectId: FV3cmDI1PW>";
sceneDataFile = "<PFFile: 0x7fc743a96920>";
}
(lldb) p results![0]
(PFObject) $R6 = 0x00007fc743a8f310 {
NSObject = {
isa = 0x00007fc743a8f310
}
...
// Info on object...
...
}
The problem here was that the Scene object was not properly registered with Parse as a PFObject subclass. The initialize method was correctly overridden like so:
override class func initialize() {
struct Static {
static var onceToken : dispatch_once_t = 0;
}
dispatch_once(&Static.onceToken) {
self.registerSubclass()
}
}
but the initalize function wasn't being called by the time the query was called. The issue is that a class's initialize method is called right before the first message is sent to that class, but there was no message being sent to Scene before the query was performed. Adding the following line to my AppDelegate's application(didFinishLaunchingWithOptions:) solved the issue
Scene.initialize()
After that, Parse was able to infer which class on the client the incoming data the objects should be, and constructed the results array with that class.

It's simple after all. What worked for me is a simple assignment of the returned PFObject to the PFObject's Objective-C subclass. So in your case,
[PFCloud callFunctionInBackground:#"func" withParameters:#{} block:^(id objects, NSError *error) {
for (PFObject *object in objects) {
MediaFile *mf = object; // simple as that
[array addObject:mf];
}
}];

Related

Getting mutable array of dictionaries then adding objects in Objective-C

I'm translating two different apps from Swift to Objective - C and one thing I'm hung up on is a global property of an array of mutable dictionaries.
Here is the syntax and Swift and the code where the objects are added,
1st app (adding places to a dictionary for a map app)
declared at top of class
var places = [Dictionary<String, String>()]
objects added
places.append(["name":"Taj Mahal","lat":"27.175277","lon":"78.042128"])
2nd app (check to see if a userID on parse is following another user)
declared at top of class
var isFollowing = ["":false]
objects added
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let objects = objects {
if objects.count > 0 {
self.isFollowing[user.objectId!] = true
} else {
self.isFollowing[user.objectId!] = false
}
}
To do this in Objective-C is it necessary to set up an NSObject subclass with an initWithDictionary method?
Any suggestions? Thanks in advance!
No you won't need to create a custom class to do this.
Create the class-level array by defining this in the SomeClass.m file:
static NSMutableArray *_places = nil;
Use a singleton-pattern to initialize and retrieve it:
+ (NSMutableArray *)places
{
if (!_places)
_places = [NSMutableArray new];
return _places;
}
Add to it like this:
NSMutableArray *places = [SomeClass places];
[places addObject: #{
#"name" : #"Taj Mahal",
#"lat" : #(27.175277), // Note: NSNumber, not NSString
#"lon" : #(78.042128) // ditto
}];
The second app doesn't need a global array by the look of it; simply a #property containing the user-id:
#interface SomeClass : NSObject
#property NSUInteger followingUserId;
#end
And then set/compare the user-id as necessary:
NSUInteger incomingUserId = ...;
if (incomingUserId != self.followingUserId) {
// Set new user-id?
self.followingUserId = incomingUserId;
}

How can I automatically initialize mutable arrays/ dictionaries for my PFObject subclass? (Parse.com framework)

I have the following code in various parts of my app:
MyPFObjectSubclass *instance = [MyPFObjectSubclass object];
instance.myMutableArray = [NSMutableArray array];
instance.myMutableDictionary = [NSMutableDictionary array];
I am constantly forgetting to initialize these types, and running into problems later when I attempt setValue:forKey or addObject methods.
It's a nice-to-have, I admit, but I would like to play it safe and roll these initializations into +object if possible.
My PFObject subclasses all look roughly like this. I call [MyPFObject tlObject] to create a new instance.
#implementation MyPFObject
#dynamic objectUUID;
// Other fields
+ (MyPFObject*) tlObject
{
MyPFObject* obj = [self object];
[obj tlObjectInit];
// Other initializations
return obj;
}
+ (NSString*) parseClassName
{
return #"MyPFObject";
}
+ (PFQuery*) query
{
PFQuery* query = [PFQuery queryWithClassName: [self parseClassName]];
// Add includeKeys here
return query;
}
#end
I have a category on PFObject that includes tlObjectInit. The field objectUUID is there so that I have a value that can uniquely identify the object BEFORE IT IS SAVED. This is necessary because I sometimes create sets of objects that refer to one another. The Parse objectID is not set until it is saved.
#implementation PFObject (TL)
- (void) tlObjectInit
{
NSString* format = [[self parseClassName] stringByAppendingString: #"-%#"];
[self setObject: [NSUUID uuidStringInFormat: format]
forKey: #"objectUUID"];
}
// Add other initializations here
#end
In your MyPFObjectSubclass, override the init method:
-(instancetype) init
{
if (self = [super init])
{
_myMutableArray = [NSMutableArray array];
_myMutableDictionary = [NSMutableDictionary dictionary];
}
return self;
}
I guess your object method call one way or the other the init method.
Edit:
It looks like you use the Parse framework. As told in the reference, the PFObject init method shouldn't be overridden by subclasses.

PFQueryTableView trouble populating table view

I'm currently working on a PFQueryTableView and trying to get it to populate with data from an array that's pulled from ViewDidLoad. UPDATE: I've moved the function to an NSObject and implemented a singleton to be used across multiple classes in an effort to silo the operation away from the view controller. Below is the updated code:
+ (NSArray *)savedTankArray
{
PFUser *userName = [PFUser currentUser];
NSString *userNameString = [userName objectForKey:#"username"];
PFQuery *query = [[PFQuery alloc] initWithClassName:#"SavedTanks"];
[query whereKey:#"userName" equalTo:userNameString];
[query setValue:#"SavedTanks" forKeyPath:#"parseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error)
{
if (!error)
{
// The find succeeded.
NSLog(#"Successfully retrieved %lu Tanks.", objects.count);
// Do something with the found objects
for (PFObject *object in objects)
{
NSString *tankNameString = [[NSString alloc] init];
NSString *tankCapacityString = [[NSString alloc] init];
tankNameString = [object valueForKey:#"tankName"];
tankCapacityString = [object valueForKey:#"tankCapacity"];
NSLog(#"%#", tankNameString);
NSLog(#"%#", tankCapacityString);
_savedTankArray = [objects objectAtIndex:0];
}
}
else
{
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
NSLog(#"TANK NAME ARRAY: %#", _savedTankArray);
return [_savedTankArray savedTankObjects];
}
While the NSLogs inside of the function work just fine, my problem is a bit expanded now, and I feel as though I'm missing something really simple here.
By the time I get to #"TANK NAME ARRAY: %#"... obviously it's returning null because its outside of the portion that handles the query. This doesn't help me much if I'm trying to bring the data in through another class.
I've tried so much over the past few days and I can't imagine I'm missing something terribly complex. I'm sorry for re-opening this but I can't wrap my head around it at this time.
Any ideas on how I could handle this? I appreciate the help as always.
There may be other trouble, but for sure this line:
tableData = [NSArray arrayWithObjects:objects, nil];
is a mistake. This will create a single-element array whose first element is the array of results. I think you can fix and simplify as:
tableData = objects;
For your question on how to proceed, I think you can carry on in this class the way one would in any table view controller. Answer the table datasource methods by referring to tableData (i.e. it's count for numberOfRowsInSection:, and tableData[indexPath.row] to configure a cellForRowAtIndexPath:, and so on).
New answer for the edited new question:
It appears that the mixup is with calling the asynch service. I'll give two kinds of advice here. First, the simplest possible table-containing view controller that gets its data from an asynch service, and second, a little class that wraps the parse asynch service. First the VC:
// in a vc with a table view .m
#interface MyViewController ()
#property(weak,nonatomic) IBOutlet UITableView *tableView;
#property(strong,nonatomic) NSArray *array; // this class keeps the array
#end
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[ClassThatHandlesMyQuery doQuery:^(NSArray *results) {
self.array = results;
[self.tableView reloadData];
}];
}
See how the query class method in the other class takes a block parameter? This is required because the query happens asynchronously.
// do the normal table view stuff
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.array.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
PFObject *pfObject = self.array[indexPath.row];
cell.textLabel.text = [pfObject valueForKey:#"someStringProperty"];
return cell;
}
That should be pretty much all you need in the vc. Now let's look at your query method. It makes three mistakes: (a) No block parameter to let the caller get the asynch result, (b) it mishandles the array in the query completion block, (c) at the end of the method, it wrongly supposes that a variable _savedTankArray is initialized, in the block. That code appears below the block, but it actually runs before the block runs.\
Let's fix all three problems. First declare a public method:
// ClassThatHandlesMyQuery.h
+ (void) doQuery:(void (^)(NSArray *))completion;
See how it takes a block as param? Now implement:
// ClassThatHandlesMyQuery.m
+ (void) doQuery:(void (^)(NSArray *))completion {
// your query code. let's assume this is fine
PFUser *userName = [PFUser currentUser];
NSString *userNameString = [userName objectForKey:#"username"];
PFQuery *query = [[PFQuery alloc] initWithClassName:#"SavedTanks"];
[query whereKey:#"userName" equalTo:userNameString];
[query setValue:#"SavedTanks" forKeyPath:#"parseClassName"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// the job is MUCH simpler here than your code supposed.
// log the result for fun
NSLog(#"did we get objects? %#", objects);
// hand it back to the caller
// notice there's no array kept in this class. it's not needed
// and it would be awkward to do it at the class (not instance) level
completion(objects);
} else {
NSLog(#"bad news from parse: %#", error);
completion(nil);
}
}
// this is important
NSLog(#"hi mom!");
// watch your log output. 'hi mom' will appear before either message
// from the block. why is that? because that block runs later
// after the network request completes. but the hi mom NSLog runs
// just before the network request starts. this is why it's wrong to expect
// any variable set in the block to be initialized here
}
Believe it or not, that's it. You should be able to write exactly the mini view controller class and the mini query classes as described here, and see data from parse in a UITableView. I suggest you build something just like this (exactly like this) first just to get going

Saving PFObject NSCoding

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
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.
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.
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.

Parse specific, how to save query results on an NSArray

Im very new to iOS and PFQuery and I need your help please
IM trying to store the array of objects obtained form PFQuery into a local NSArray, Im trying to do it inside if (!error) but it does not leave the block, once the block terminates so does the values for it on my array.
//InstallersDirectory.m
#interface InstallersDirectoryTVC ()
#property (nonatomic, strong) NSArray *supervisors;
#end
//more code goes here
- (void)viewDidLoad
{
[super viewDidLoad];
PFQuery *query = [PFQuery queryWithClassName:#"InstallersInfo"];
[query whereKey:#"supervisor" equalTo:#"yes"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
self.supervisors = [objects valueForKey:#"supervisor"];
}
}else {
NSLog(#"Error, %# %#",error,[error userInfo]);
}
}];
Everything works inside the block, like self.supervisors.count or NSLog, but it doesn't leave the block at all. Could you please tell me how I can get those values store definitely to self.supervisors?
Thanks !!
edit | comment
If you still need help on this issue, here is a suggestion or two:
first, self.supervisors is a NSArray. As a NSArray it has to be fully initialized and populated at creation time. So, even though you are iterating through your results, you are creating a new array each time which means only the last iteration would likely be stored in self.supervisors.
try this instead,
#property (nonatomic,strong) NSMutableArray *supervisors;
in your block:
[self.supervisors addObject:object];
//note: this will put the entire returned object in your mutable array
then later outside your block:
to list all the supervisors:
for (PFObject *supervisor in self.supervisors) {
NSLog(#"supervisor info:%#",supervisor);
}
To get the 4th supervisor in the list:
PFObject *superVisorNumber4 = [self.supervisors objectAtIndex:4];
To get the phone number from that supervisor (making this up :)
NSString *phone = [superVisorNumber4 objectForKey:#"phone"];
hope that helps

Resources