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.
Related
I'm try to store CCLocation in CoreData.
I'm on beginning of write my custom value transformer, but i'm stuck.
My transformer:
#import "LocationToDataTransformer.h"
#import <CoreLocation/CoreLocation.h>
#implementation LocationToDataTransformer
+ (BOOL)allowsReverseTransformation {
return YES;
}
+ (Class)transformedValueClass {
NSLog(#"tranform");
return [NSData class];
}
- (id)transformedValue:(id)value {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:value];
NSLog(#"tranform");
return data;
}
- (id)reverseTransformedValue:(id)value {
CLLocation *location = (CLLocation*)[NSKeyedUnarchiver unarchiveObjectWithData:value];
NSLog(#"tranform");
return location;
}
#end
And transformer registration:
+ (void)initialize {
LocationToDataTransformer *transformer = [[LocationToDataTransformer alloc] init];
[NSValueTransformer setValueTransformer:transformer forName:#"LocationToDataTransformer"];
}
I've check [NSValueTransformer valueTransformerNames] array and i see my transformer name in it.
In model I have a entity with 3 attributes:
title (string)
subtitle (string)
location (transformable) - value transformer
name:LocationToDataTransformer
And when I'm saving my location, nothing happens, other attributes of entity (strings) saves, but location is nil.
Saving entity code:
-(void)saveBookmark:(BookmarkEntity*)bookmark {
NSManagedObject *object = [[NSManagedObject alloc]initWithEntity:_descr insertIntoManagedObjectContext:_context];
BookmarkEntity* entity = (BookmarkEntity*)object;
entity.title = bookmark.title;
entity.subtitle = bookmark.subtitle;
entity.location = bookmark.location;
if (_context.hasChanges) {
NSError* error;
bool saved = [_context save:&error];
if (!saved) {
NSLog(#"Error while saving: %#",error.localizedDescription);
}
}
}
As you can see i've added NSLog's to my transformer, but i not see it in logs. Also i try breakpoints, and no one method from my transformer don't calls.
I even try to set transformer name to "abracadabra" and see nothing in logs, nothing about wrong value transformer name.
I'm totally confused. Anyone have an idea where i missed up? Thanks.
Your transformer is the wrong way around.
I'm working on a app that can randomize love couples. Just a fun thing, okey!?!? :D
But the problem, or maybe not a problem but a thing that can be much better if I get this thing to be working. In the beginning you need to write in all the names. And thats takes some time... Should I use Core Date? I don't really knows what core data is so I'm not sure. I would love if a god come to me and wrote the full code that can remember an array even if the app and phone shuts down. I have done this in java, is that simpel that it is in java? That would be great!
//Thank, Anton
For Heavy, complex data structures you would want to use core data,
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdTechnologyOverview.html#//apple_ref/doc/uid/TP40009296-SW1
But seeing as you just want to store an array, You should look into NSUserDefaults.
NSUserDefaults will store given data as long as the app is not deleted. You will most likely want to create some kind of custom DataStorage class for this.
#interface DataStorage : NSObject <NSCoding>
#property (nonatomic, strong) NSMutableArray *arrayToStore;
+ (instancetype)sharedInstance;
- (void)save;
#end
Above is the .h file. As you can see, it follows NSCoding protocols. That provides access to methods which allow you to encode data. You will use the save method to write the data to disk.
#import "DataStorage.h"
#implementation DataStorage
#synthesize arrayOfPeople = _arrayToStore;
+ (DataStorage *)sharedInstance
{
static DataStorage *state = nil;
if ( !state )
{
NSData *data =[[NSUserDefaults standardUserDefaults] objectForKey:#"DataStorageKey"];
if (data)
{
state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
}
else
{
state = [[DataStorage alloc] init];
}
}
return state;
}
- (id)init{
if (self = [super init]) {
if (!_arrayToStore) {
_arrayToStore = [[NSMutableArray alloc] init];
}
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)decoder
{
self = [self init];
if (self) {
if ([decoder decodeObjectForKey:#"DataStorageArrayToStore"]) {
_arrayToStore = [[decoder decodeObjectForKey:#"DataStorageArrayToStore"] mutableCopy];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_arrayToStore forKey:#"DataStorageArrayToStore"];
}
- (void)save
{
NSData *appStateData = [NSKeyedArchiver archivedDataWithRootObject:self];
[[NSUserDefaults standardUserDefaults] setObject:appStateData forKey:#"DataStorageKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#end
Here is the .m file, which pretty much evaluates to see if there is a saved instance of the class, and if not it will create one. [DataStorage sharedInstance]...
when you want to store some data, you will simply make the class available to said file, #import "DataStorage.m and then use
NSString *testData = [NSString stringWithFormat: #"Test Data String"];
[[DataStorage sharedInstance].arrayToStore addObject: testData];
[DataStorage sharedInstance] save];
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.
I need to make a "clone" of an array to another, but the thing is that when I modify my copied Array, the original is modified too. Using hard copy is not working as I expect.
I'm initializing an array like this:
NSMutableArray *otherArray = [[NSMutableArray alloc] initWithArray: myList copyItems:YES];
where myList is a NSArray that came as a parameter in my function.
The thing is when I need to return myList, it's content has been modified when I modify my otherArray
I tried making a hard copy like:
NSMutableArray* algo = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:myList]];
But, some of the properties are not converted, and a nil value is assigned to them.
Also I tried with this:
NSMutableArray *otherArray = [myList mutableCopy];
Well, the obvious question is: How can I modify a copied object without modifying the original one?
Thanks!
EDIT: Here is my entire function.
RAC(self,filteredPacks) = [RACSignal combineLatest:#[self.searchBoxSignal, self.packListsSignal]
reduce:^NSArray *(NSString *filterString, NSArray *packList) {
NSMutableArray *sweetHelper = [[NSMutableArray alloc] init];
NSMutableArray* packListCopy = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:packList]];
filterString = [filterString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([filterString length] > 0)
{
for(PackList *theList in packListCopy){
NSMutableIndexSet *indexesToDelete = [NSMutableIndexSet indexSet];
NSUInteger currentIndex = 0;
for(Pack *thePack in theList.resolved_packs){
if([thePack.name rangeOfString:filterString options:NSCaseInsensitiveSearch].location == NSNotFound){
[indexesToDelete addIndex:currentIndex];
}
currentIndex++;
}
[theList.resolved_packs removeObjectsAtIndexes:indexesToDelete];
[theList.packs removeObjectsAtIndexes:indexesToDelete];
[sweetHelper addObject:theList];
}
return sweetHelper;
}
else
{
return self.originalList;
}
}
];
Well, I found the solution using this answer and this comment in the same question.
As I said in my question:
"I tried making a hard copy like:
NSMutableArray* algo = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:myList]];
But, some of the properties are not converted, and a nil value is assigned to them."
Well, the properties that were not converted, they weren't because they are custom objects, An I didn't implemented the initWithCode and encodeWithCoder methods. So, when I added those methods to my customObjectClass, my Arrays were hard copied with all their elements.
A little example about what I did:
In my SomeCustomObject.h I should implements NSCoding:
#interface SomeCustomObject : NSObject <NSCoding> {
NSMutableArray * __packs;
int __type;
Link * __selfRef;
NSMutableArray * __resolved_packs;
}
And in my subclass of SomeCustomObject I got something like:
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
__packs = [decoder decodeObjectForKey:#"packs"];
__type = [decoder decodeIntForKey:#"type"];
__selfRef = [decoder decodeObjectForKey:#"selfRef"];
__resolved_packs = [decoder decodeObjectForKey:#"resolved_packs"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:__packs forKey:#"packs"];
[encoder encodeInt:__type forKey:#"type"];
[encoder encodeObject:__selfRef forKey:#"selfRef"];
[encoder encodeObject:__resolved_packs forKey:#"resolved_packs"];
}
I hope this be useful to somebody :)
I have to send a data by post in JSON format. I have my nsdictionary with keys and values.
NSDictionary *params_country=[NSDictionary dictionaryWithObjectsAndKeys:
#"1111",#"#id",
nil];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
#"dummy3", #"#name",
#"dummy3#example.com", #"#mail",
#"password",#"#password", params_country,#"country",
nil];
When i am doing a log
DLog(#"params %#",[params description]);
I am getting the following
params {
"#mail" = "dummy3#example.com";
"#name" = dummy3;
"#password" = password;
}
The problem is that i have to sent the JSON in the order that i have listed in the above initialisation of my nsdictionary but the keys are being sorted somehow.
Any solution?
EDIT
Sorry i am sending a nsdictionary also in the params. If i remove the country then its fine.
Dictionaries are an unordered collection type. If you need to maintain a certain order, then you should use an ordered collection type like NSArray. But for this, your web service shouldn't care about the order, since it should be looking up the values by the keys provided.
As per some of the comments, this requirement does not match a valid JSON object as the official JSON Specification states:
An object is an unordered set of name/value pairs. An object begins with { (left brace) and ends with } (right brace). Each name is followed by : (colon) and the name/value pairs are separated by , (comma).
Unfortunately we don't live in a perfect world with perfect web services and there are often certain things that are out of our control.
I wrote a subclass of NSMutableDictionary after reading up on the internet that will order the dictionary based on the order you call setValue:forKey:.
I put the class into a gist you can download from here: https://gist.github.com/liamnichols/7869468 or you can just copy it from below:
LNOrderedMutableDictionary.h
#interface LNOrderedMutableDictionary : NSMutableDictionary
///If `anObject` is nil, it will not be added to the dictionary.
- (void)setNothingIfNil:(id)anObject forKey:(id)aKey;
#end
LNOrderedMutableDictionary.m
#import "LNOrderedMutableDictionary.h"
#interface LNOrderedMutableDictionary ()
#property (nonatomic, strong) NSMutableDictionary *dictionary;
#property (nonatomic, strong) NSMutableOrderedSet *array;
#end
#implementation LNOrderedMutableDictionary
- (id)initWithCapacity:(NSUInteger)capacity
{
self = [super init];
if (self != nil)
{
self.dictionary = [[NSMutableDictionary alloc] initWithCapacity:capacity];
self.array = [[NSMutableOrderedSet alloc] initWithCapacity:capacity];
}
return self;
}
- (id)init
{
self = [self initWithCapacity:0];
if (self)
{
}
return self;
}
- (void)setObject:(id)anObject forKey:(id)aKey
{
[self.array removeObject:aKey];
[self.array addObject:aKey];
[self.dictionary setObject:anObject forKey:aKey];
}
- (void)setNothingIfNil:(id)anObject forKey:(id)aKey
{
if (anObject != nil)
[self setObject:anObject forKey:aKey];
}
- (void)removeObjectForKey:(id)aKey
{
[self.dictionary removeObjectForKey:aKey];
[self.array removeObject:aKey];
}
- (NSUInteger)count
{
return [self.dictionary count];
}
- (id)objectForKey:(id)aKey
{
return [self.dictionary objectForKey:aKey];
}
- (NSEnumerator *)keyEnumerator
{
return [self.array objectEnumerator];
}
#end
If possible, your web service shouldn't have to rely on the JSON objects to be formatted in a specific order but if there is nothing you can do to change this then the above solution is what you are looking for.
Source: cocoawithlove